diff --git a/.cirrus.yml b/.cirrus.yml
deleted file mode 100644
index b35984c3058..00000000000
--- a/.cirrus.yml
+++ /dev/null
@@ -1,41 +0,0 @@
-freebsd_task:
- name: FreeBSD
- timeout_in: 20m
- compute_engine_instance:
- image_project: freebsd-org-cloud-dev
- image: family/freebsd-13-0
- platform: freebsd
- cpu: 2
- memory: 4G
- env:
- NERDCTL_RUN_ARGS: --net none dougrabson/freebsd-minimal:13.1 echo "Nerdctl is up and running."
- install_script:
- - pkg install -y go containerd runj
- test_script:
- - daemon -o containerd.out containerd
- - go test -v ./pkg/...
- - cd cmd/nerdctl
- - sudo go run . run $NERDCTL_RUN_ARGS | grep running
-# TODO: run `go test -v ./cmd/...`
-
-windows_task:
- name: "Windows"
- timeout_in: 20m
- compute_engine_instance:
- image_project: cirrus-images
- image: family/windows-docker-builder
- platform: windows
- cpu: 2
- memory: 4G
- matrix:
- - name: "Windows/containerd-1.7"
- env:
- ctrdVersion: 1.7.1
- env:
- CGO_ENABLED: 0
- build_script:
- - mkdir "C:\Windows\system32\config\systemprofile\AppData\Local\Temp\"
- - powershell hack/configure-windows-ci.ps1
- - refreshenv
- - go install .\cmd\nerdctl\
- - go test -v ./cmd/...
diff --git a/.dockerignore b/.dockerignore
index 33c9733d860..646cd932d8f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,7 +3,7 @@
_output
# golangci-lint
-build
+/build
# vagrant
/.vagrant
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0fedec74806..0c21fa19f87 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -14,6 +14,26 @@ updates:
directory: "/" # Location of package manifests
schedule:
interval: "daily"
+ groups:
+ golang-x:
+ patterns:
+ - "golang.org/x/*"
+ moby-sys:
+ patterns:
+ - "github.com/moby/sys/*"
+ docker:
+ patterns:
+ - "github.com/docker/docker"
+ - "github.com/docker/cli"
+ containerd:
+ patterns:
+ - "github.com/containerd/containerd"
+ - "github.com/containerd/containerd/api"
+ stargz:
+ patterns:
+ - "github.com/containerd/stargz-snapshotter"
+ - "github.com/containerd/stargz-snapshotter/estargz"
+ - "github.com/containerd/stargz-snapshotter/ipfs"
# Dependencies listed in .github/workflows/*.yml
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml
index f8d2a0ab68b..8cc17c5287d 100644
--- a/.github/workflows/ghcr-image-build-and-publish.yml
+++ b/.github/workflows/ghcr-image-build-and-publish.yml
@@ -1,4 +1,4 @@
-name: Container Image Build
+name: image
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
@@ -12,6 +12,8 @@ on:
tags: ['v*.*.*']
pull_request:
branches: [main]
+ paths-ignore:
+ - '**.md'
env:
# Use docker.io for Docker Hub if empty
@@ -19,30 +21,29 @@ env:
# github.repository as /
IMAGE_NAME: ${{ github.repository }}
-
jobs:
build:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
- uses: actions/checkout@v3.5.3
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@53851d14592bedcffcf25ea515637cff71ef929a # v3.3.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
- uses: docker/login-action@v2.2.0
+ uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -52,14 +53,14 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
- uses: docker/metadata-action@v4.6.0
+ uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
- uses: docker/build-push-action@v4.1.1
+ uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
with:
context: .
platforms: linux/amd64,linux/arm64
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 00000000000..b98ddafac52
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,76 @@
+name: lint
+
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ pull_request:
+
+env:
+ GO_VERSION: 1.23.x
+
+jobs:
+ go:
+ timeout-minutes: 5
+ name: "go | ${{ matrix.goos }} | ${{ matrix.canary }}"
+ runs-on: "${{ matrix.os }}"
+ defaults:
+ run:
+ shell: bash
+ strategy:
+ matrix:
+ include:
+ - os: ubuntu-24.04
+ goos: linux
+ - os: ubuntu-24.04
+ goos: freebsd
+ # FIXME: this is currently failing in a non-sensical way, so, running on linux instead...
+ # - os: windows-2022
+ - os: ubuntu-24.04
+ goos: windows
+ - os: ubuntu-24.04
+ goos: linux
+ # This allows the canary script to select any upcoming golang alpha/beta/RC
+ canary: go-canary
+ env:
+ GOOS: "${{ matrix.goos }}"
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - name: Set GO env
+ run: |
+ # If canary is specified, get the latest available golang pre-release instead of the major version
+ if [ "$canary" != "" ]; then
+ . ./hack/build-integration-canary.sh
+ canary::golang::latest
+ fi
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@ec5d18412c0aeab7936cb16880d708ba2a64e1ae # v6.2.0
+ with:
+ args: --verbose
+ other:
+ timeout-minutes: 5
+ name: yaml | shell | imports order
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - name: yaml
+ run: make lint-yaml
+ - name: shell
+ run: make lint-shell
+ - name: go imports ordering
+ run: |
+ go install -v github.com/incu6us/goimports-reviser/v3@latest
+ make lint-imports
diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml
new file mode 100644
index 00000000000..bdb34f52d5f
--- /dev/null
+++ b/.github/workflows/project.yml
@@ -0,0 +1,31 @@
+name: project
+
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ pull_request:
+
+jobs:
+ project:
+ name: checks
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ path: src/github.com/containerd/nerdctl
+ fetch-depth: 100
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ cache-dependency-path: src/github.com/containerd/nerdctl
+ - uses: containerd/project-checks@434a07157608eeaa1d5c8d4dd506154204cd9401 # v1.1.0
+ with:
+ working-directory: src/github.com/containerd/nerdctl
+ repo-access-token: ${{ secrets.GITHUB_TOKEN }}
+ - run: ./hack/verify-no-patent.sh
+ working-directory: src/github.com/containerd/nerdctl
+ - run: ./hack/verify-pkg-isolation.sh
+ working-directory: src/github.com/containerd/nerdctl
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 52dd9a4f91a..b3a17aa0813 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,17 +5,15 @@ on:
tags:
- 'v*'
- 'test-action-release-*'
-env:
- GO111MODULE: on
jobs:
release:
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- - uses: actions/checkout@v3.5.3
- - uses: actions/setup-go@v4
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
- go-version: 1.20.x
+ go-version: 1.23.x
- name: "Compile binaries"
run: make artifacts
- name: "SHA256SUMS"
@@ -26,11 +24,8 @@ jobs:
run: (cd _output; sha256sum SHA256SUMS)
- name: "Prepare the release note"
run: |
- tag="${GITHUB_REF##*/}"
shasha=$(sha256sum _output/SHA256SUMS | awk '{print $1}')
cat <<-EOF | tee /tmp/release-note.txt
- ${tag}
-
$(hack/generate-release-note.sh)
- - -
The binaries were built automatically on GitHub Actions.
@@ -45,6 +40,4 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF##*/}"
- asset_flags=()
- for f in _output/*; do asset_flags+=("-a" "$f"); done
- hub release create "${asset_flags[@]}" -F /tmp/release-note.txt --draft "${tag}"
+ gh release create -F /tmp/release-note.txt --draft --title "${tag}" "${tag}" _output/*
diff --git a/.github/workflows/test-canary.yml b/.github/workflows/test-canary.yml
new file mode 100644
index 00000000000..c178b907df4
--- /dev/null
+++ b/.github/workflows/test-canary.yml
@@ -0,0 +1,98 @@
+# This pipeline purpose is solely meant to run a subset of our test suites against upcoming or unreleased dependencies versions
+name: canary
+
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ pull_request:
+ paths-ignore:
+ - '**.md'
+
+env:
+ UBUNTU_VERSION: "24.04"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+ linux:
+ runs-on: "ubuntu-24.04"
+ timeout-minutes: 40
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - name: "Prepare integration test environment"
+ run: |
+ . ./hack/build-integration-canary.sh
+ canary::build::integration
+ - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)"
+ run: |
+ sudo systemctl disable --now snapd.service snapd.socket
+ sudo apt-get purge -y snapd
+ sudo losetup -Dv
+ sudo losetup -lv
+ - name: "Register QEMU (tonistiigi/binfmt)"
+ run: |
+ # `--install all` will only install emulation for architectures that cannot be natively executed
+ # Since some arm64 platforms do provide native fallback execution for 32 bits,
+ # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`.
+ # To avoid that, we explicitly list the architectures we do want emulation for.
+ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
+ - name: "Run unit tests"
+ run: go test -v ./pkg/...
+ - name: "Run integration tests"
+ run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false
+ - name: "Run integration tests (flaky)"
+ run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true
+
+ windows:
+ timeout-minutes: 30
+ runs-on: windows-latest
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - name: Set GO env
+ run: |
+ # Get latest containerd
+ args=(curl --proto '=https' --tlsv1.2 -fsSL -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28")
+ [ "${GITHUB_TOKEN:-}" == "" ] && {
+ >&2 printf "GITHUB_TOKEN is not set - you might face rate limitations with the Github API\n"
+ } || args+=(-H "Authorization: Bearer $GITHUB_TOKEN")
+ ctd_v="$("${args[@]}" https://api.github.com/repos/containerd/containerd/tags | jq -rc .[0].name)"
+ echo "CONTAINERD_VERSION=${ctd_v:1}" >> "$GITHUB_ENV"
+
+ . ./hack/build-integration-canary.sh
+ canary::golang::latest
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - run: go install ./cmd/nerdctl
+ - run: go install -v gotest.tools/gotestsum@v1
+ # This here is solely to get the cni install script, which has not been modified in 3+ years.
+ # There is little to no reason to update this to latest containerd
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ repository: containerd/containerd
+ ref: "v1.7.25"
+ path: containerd
+ fetch-depth: 1
+ - name: "Set up CNI"
+ working-directory: containerd
+ run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows
+ # Windows setup script can only use released versions
+ - name: "Set up containerd"
+ env:
+ ctrdVersion: ${{ env.CONTAINERD_VERSION }}
+ run: powershell hack/configure-windows-ci.ps1
+ - name: "Run integration tests"
+ run: ./hack/test-integration.sh -test.only-flaky=false
+ - name: "Run integration tests (flaky)"
+ run: ./hack/test-integration.sh -test.only-flaky=true
diff --git a/.github/workflows/test-kube.yml b/.github/workflows/test-kube.yml
new file mode 100644
index 00000000000..580a9a2181a
--- /dev/null
+++ b/.github/workflows/test-kube.yml
@@ -0,0 +1,27 @@
+# This pipeline purpose is solely meant to run a subset of our test suites against a kubernetes cluster
+name: kubernetes
+
+on:
+ push:
+ branches:
+ - main
+ - 'release/**'
+ pull_request:
+ paths-ignore:
+ - '**.md'
+
+jobs:
+ linux:
+ runs-on: "ubuntu-24.04"
+ timeout-minutes: 40
+ env:
+ ROOTFUL: true
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - name: "Run Kubernetes integration tests"
+ # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization
+ run: |
+ ./hack/build-integration-kubernetes.sh
+ sudo ./_output/nerdctl exec nerdctl-test-control-plane bash -c -- 'export TMPDIR="$HOME"/tmp; mkdir -p "$TMPDIR"; cd /nerdctl-source; /usr/local/go/bin/go test -p 1 ./cmd/nerdctl/... -test.only-kubernetes'
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fb243e08431..8f634ee93d3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,93 +6,195 @@ on:
- main
- 'release/**'
pull_request:
+ paths-ignore:
+ - '**.md'
env:
- GO_VERSION: 1.20.x
+ GO_VERSION: 1.23.x
+ SHORT_TIMEOUT: 5
+ LONG_TIMEOUT: 60
jobs:
- project:
- name: Project Checks
- runs-on: ubuntu-22.04
- timeout-minutes: 20
- steps:
- - uses: actions/checkout@v3.5.3
- with:
- path: src/github.com/containerd/nerdctl
- fetch-depth: 100
- - uses: actions/setup-go@v4
- with:
- go-version: ${{ env.GO_VERSION }}
- cache-dependency-path: src/github.com/containerd/nerdctl
- - uses: containerd/project-checks@v1.1.0
- with:
- working-directory: src/github.com/containerd/nerdctl
- repo-access-token: ${{ secrets.GITHUB_TOKEN }}
- - run: ./hack/verify-no-patent.sh
- working-directory: src/github.com/containerd/nerdctl
- - run: ./hack/verify-pkg-isolation.sh
- working-directory: src/github.com/containerd/nerdctl
-
- lint:
- runs-on: ubuntu-22.04
- timeout-minutes: 20
+ # This job builds the dependency target of the test docker image for all supported architectures and cache it in GHA
+ build-dependencies:
+ timeout-minutes: 15
+ name: dependencies | ${{ matrix.containerd }} | ${{ matrix.arch }}
+ runs-on: "${{ matrix.runner }}"
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: ubuntu-24.04
+ containerd: v1.6.36
+ arch: amd64
+ - runner: ubuntu-24.04
+ containerd: v1.7.25
+ arch: amd64
+ - runner: ubuntu-24.04
+ containerd: v2.0.2
+ arch: amd64
+ - runner: arm64-8core-32gb
+ containerd: v2.0.2
+ arch: arm64
+ env:
+ CONTAINERD_VERSION: "${{ matrix.containerd }}"
+ ARCH: "${{ matrix.arch }}"
steps:
- - uses: actions/checkout@v3.5.3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- - uses: actions/setup-go@v4
- with:
- go-version: ${{ env.GO_VERSION }}
- check-latest: true
- cache: true
- - name: golangci-lint
- uses: golangci/golangci-lint-action@v3.6.0
- with:
- version: v1.51.1
- args: --verbose
- - name: yamllint-lint
- run: yamllint .
+ - name: "Expose GitHub Runtime variables for gha"
+ uses: crazy-max/ghaction-github-runtime@b3a9207c0e1ef41f4cf215303c976869d0c2c1c4 # v3.0.0
+ - name: "Build dependencies for the integration test environment image"
+ run: |
+ docker buildx create --name with-gha --use
+ docker buildx build \
+ --output=type=docker \
+ --cache-to type=gha,mode=max,scope=${ARCH}-${CONTAINERD_VERSION} \
+ --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \
+ --target build-dependencies --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
test-unit:
- runs-on: ubuntu-22.04
- timeout-minutes: 20
+ # FIXME:
+ # Supposed to work: https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions#example-returning-a-json-data-type
+ # Apparently does not
+ # timeout-minutes: ${{ fromJSON(env.SHORT_TIMEOUT) }}
+ timeout-minutes: 10
+ name: unit | ${{ matrix.goos }}
+ runs-on: "${{ matrix.os }}"
+ defaults:
+ run:
+ shell: bash
+ strategy:
+ matrix:
+ include:
+ - os: windows-2022
+ goos: windows
+ - os: ubuntu-24.04
+ goos: linux
steps:
- - uses: actions/checkout@v3.5.3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: ${{ env.GO_VERSION }}
check-latest: true
- cache: true
+ - if: ${{ matrix.goos=='windows' }}
+ uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ repository: containerd/containerd
+ ref: v1.7.25
+ path: containerd
+ fetch-depth: 1
+ - if: ${{ matrix.goos=='windows' }}
+ name: "Set up CNI"
+ working-directory: containerd
+ run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows
- name: "Run unit tests"
- run: go test -v ./pkg/...
+ run: make test-unit
test-integration:
- runs-on: "ubuntu-${{ matrix.ubuntu }}"
- timeout-minutes: 40
+ needs: build-dependencies
+ timeout-minutes: 30
+ name: rootful | ${{ matrix.containerd }} | ${{ matrix.runner }}
+ runs-on: "${{ matrix.runner }}"
strategy:
fail-fast: false
matrix:
- # ubuntu-20.04: cgroup v1, ubuntu-22.04: cgroup v2
+ # ubuntu-20.04: cgroup v1, ubuntu-22.04 and later: cgroup v2
include:
- ubuntu: 20.04
- containerd: v1.6.21
- - ubuntu: 20.04
- containerd: v1.7.1
- - ubuntu: 22.04
- containerd: v1.7.1
+ containerd: v1.6.36
+ runner: "ubuntu-20.04"
+ arch: amd64
- ubuntu: 22.04
- containerd: main
+ containerd: v1.7.25
+ runner: "ubuntu-22.04"
+ arch: amd64
+ - ubuntu: 24.04
+ containerd: v2.0.2
+ runner: "ubuntu-24.04"
+ arch: amd64
+ - ubuntu: 24.04
+ containerd: v2.0.2
+ runner: arm64-8core-32gb
+ arch: arm64
env:
+ CONTAINERD_VERSION: "${{ matrix.containerd }}"
+ ARCH: "${{ matrix.arch }}"
UBUNTU_VERSION: "${{ matrix.ubuntu }}"
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - name: "Expose GitHub Runtime variables for gha"
+ uses: crazy-max/ghaction-github-runtime@b3a9207c0e1ef41f4cf215303c976869d0c2c1c4 # v3.0.0
+ - name: "Prepare integration test environment"
+ run: |
+ docker buildx create --name with-gha --use
+ docker buildx build \
+ --output=type=docker \
+ --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \
+ -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
+ - name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)"
+ run: |
+ sudo systemctl disable --now snapd.service snapd.socket
+ sudo apt-get purge -y snapd
+ sudo losetup -Dv
+ sudo losetup -lv
+ - name: "Register QEMU (tonistiigi/binfmt)"
+ run: |
+ # `--install all` will only install emulation for architectures that cannot be natively executed
+ # Since some arm64 platforms do provide native fallback execution for 32 bits,
+ # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`.
+ # To avoid that, we explicitly list the architectures we do want emulation for.
+ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
+ - name: "Run integration tests"
+ run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false
+ - name: "Run integration tests (flaky)"
+ run: docker run -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true
+
+ test-integration-ipv6:
+ needs: build-dependencies
+ timeout-minutes: 15
+ name: ipv6 | ${{ matrix.containerd }} | ${{ matrix.ubuntu }}
+ runs-on: "ubuntu-${{ matrix.ubuntu }}"
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - ubuntu: 24.04
+ containerd: v2.0.2
+ arch: amd64
+ env:
CONTAINERD_VERSION: "${{ matrix.containerd }}"
+ ARCH: "${{ matrix.arch }}"
+ UBUNTU_VERSION: "${{ matrix.ubuntu }}"
steps:
- - uses: actions/checkout@v3.5.3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
+ - name: Enable ipv4 and ipv6 forwarding
+ run: |
+ sudo sysctl -w net.ipv6.conf.all.forwarding=1
+ sudo sysctl -w net.ipv4.ip_forward=1
+ - name: "Expose GitHub Runtime variables for gha"
+ uses: crazy-max/ghaction-github-runtime@b3a9207c0e1ef41f4cf215303c976869d0c2c1c4 # v3.0.0
+ - name: Enable IPv6 for Docker, and configure docker to use containerd for gha
+ run: |
+ sudo mkdir -p /etc/docker
+ echo '{"ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "experimental": true, "ip6tables": true}' | sudo tee /etc/docker/daemon.json
+ sudo systemctl restart docker
- name: "Prepare integration test environment"
- run: DOCKER_BUILDKIT=1 docker build -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
+ run: |
+ docker buildx create --name with-gha --use
+ docker buildx build \
+ --output=type=docker \
+ --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \
+ -t test-integration --target test-integration --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
- name: "Remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)"
run: |
sudo systemctl disable --now snapd.service snapd.socket
@@ -100,91 +202,225 @@ jobs:
sudo losetup -Dv
sudo losetup -lv
- name: "Register QEMU (tonistiigi/binfmt)"
- run: docker run --privileged --rm tonistiigi/binfmt --install all
+ run: |
+ # `--install all` will only install emulation for architectures that cannot be natively executed
+ # Since some arm64 platforms do provide native fallback execution for 32 bits,
+ # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`.
+ # To avoid that, we explicitly list the architectures we do want emulation for.
+ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
- name: "Run integration tests"
- run: docker run -t --rm --privileged test-integration
+ # The nested IPv6 network inside docker and qemu is complex and needs a bunch of sysctl config.
+ # Therefore, it's hard to debug why the IPv6 tests fail in such an isolation layer.
+ # On the other side, using the host network is easier at configuration.
+ # Besides, each job is running on a different instance, which means using host network here
+ # is safe and has no side effects on others.
+ run: docker run --network host -t --rm --privileged test-integration ./hack/test-integration.sh -test.only-ipv6
test-integration-rootless:
+ needs: build-dependencies
+ timeout-minutes: 30
+ name: "${{ matrix.target }} | ${{ matrix.containerd }} | ${{ matrix.rootlesskit }} | ${{ matrix.ubuntu }}"
runs-on: "ubuntu-${{ matrix.ubuntu }}"
- timeout-minutes: 60
strategy:
fail-fast: false
matrix:
- # ubuntu-22.04: cgroup v1, ubuntu-22.04: cgroup v2
+ # ubuntu-20.04: cgroup v1, ubuntu-22.04 and later: cgroup v2
include:
- ubuntu: 20.04
- containerd: v1.6.21
- target: test-integration-rootless
- - ubuntu: 20.04
- containerd: v1.7.1
- target: test-integration-rootless
- - ubuntu: 22.04
- containerd: v1.7.1
- target: test-integration-rootless
- - ubuntu: 22.04
- containerd: main
- target: test-integration-rootless
- - ubuntu: 20.04
- containerd: v1.6.21
- target: test-integration-rootless-port-slirp4netns
- - ubuntu: 20.04
- containerd: v1.7.1
- target: test-integration-rootless-port-slirp4netns
+ containerd: v1.6.36
+ rootlesskit: v1.1.1 # Deprecated
+ target: rootless
+ arch: amd64
- ubuntu: 22.04
- containerd: v1.7.1
- target: test-integration-rootless-port-slirp4netns
- - ubuntu: 22.04
- containerd: main
- target: test-integration-rootless-port-slirp4netns
+ containerd: v1.7.25
+ rootlesskit: v2.3.2
+ target: rootless
+ arch: amd64
+ - ubuntu: 24.04
+ containerd: v2.0.2
+ rootlesskit: v2.3.2
+ target: rootless
+ arch: amd64
+ - ubuntu: 24.04
+ containerd: v1.7.25
+ rootlesskit: v2.3.2
+ target: rootless-port-slirp4netns
+ arch: amd64
env:
- UBUNTU_VERSION: "${{ matrix.ubuntu }}"
CONTAINERD_VERSION: "${{ matrix.containerd }}"
- TEST_TARGET: "${{ matrix.target }}"
+ ARCH: "${{ matrix.arch }}"
+ UBUNTU_VERSION: "${{ matrix.ubuntu }}"
+ ROOTLESSKIT_VERSION: "${{ matrix.rootlesskit }}"
+ TEST_TARGET: "test-integration-${{ matrix.target }}"
steps:
- - uses: actions/checkout@v3.5.3
+ - name: "Set up AppArmor"
+ if: matrix.ubuntu == '24.04'
+ run: |
+ cat <,
+ include
+
+ /usr/local/bin/rootlesskit flags=(unconfined) {
+ userns,
+
+ # Site-specific additions and overrides. See local/README for details.
+ include if exists
+ }
+ EOT
+ sudo systemctl restart apparmor.service
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- name: "Register QEMU (tonistiigi/binfmt)"
- run: docker run --privileged --rm tonistiigi/binfmt --install all
+ run: |
+ # `--install all` will only install emulation for architectures that cannot be natively executed
+ # Since some arm64 platforms do provide native fallback execution for 32 bits,
+ # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`.
+ # To avoid that, we explicitly list the architectures we do want emulation for.
+ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
+ - name: "Expose GitHub Runtime variables for gha"
+ uses: crazy-max/ghaction-github-runtime@b3a9207c0e1ef41f4cf215303c976869d0c2c1c4 # v3.0.0
- name: "Prepare (network driver=slirp4netns, port driver=builtin)"
- run: DOCKER_BUILDKIT=1 docker build -t ${TEST_TARGET} --target ${TEST_TARGET} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} .
+ run: |
+ docker buildx create --name with-gha --use
+ docker buildx build \
+ --output=type=docker \
+ --cache-from type=gha,scope=${ARCH}-${CONTAINERD_VERSION} \
+ -t ${TEST_TARGET} --target ${TEST_TARGET} --build-arg UBUNTU_VERSION=${UBUNTU_VERSION} --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} --build-arg ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION} .
+ - name: "Disable BuildKit for RootlessKit v1 (workaround for issue #622)"
+ run: |
+ # https://github.com/containerd/nerdctl/issues/622
+ WORKAROUND_ISSUE_622=
+ if echo "${ROOTLESSKIT_VERSION}" | grep -q v1; then
+ WORKAROUND_ISSUE_622=1
+ fi
+ echo "WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622}" >> "$GITHUB_ENV"
- name: "Test (network driver=slirp4netns, port driver=builtin)"
- run: docker run -t --rm --privileged ${TEST_TARGET}
+ run: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false
+ - name: "Test (network driver=slirp4netns, port driver=builtin) (flaky)"
+ run: docker run -t --rm --privileged -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622} ${TEST_TARGET} /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true
- cross:
- runs-on: ubuntu-22.04
- timeout-minutes: 40
+ build:
+ timeout-minutes: 5
+ name: "build | ${{ matrix.go-version }}"
+ runs-on: ubuntu-24.04
strategy:
matrix:
- go-version: ["1.19.x", "1.20.x"]
+ go-version: ["1.22.x", "1.23.x"]
steps:
- - uses: actions/checkout@v3.5.3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: ${{ matrix.go-version }}
- cache: true
check-latest: true
- - name: "Cross"
- run: GO_VERSION="$(echo ${{ matrix.go-version }} | sed -e s/.x//)" make artifacts
+ - name: "build"
+ run: GO_VERSION="$(echo ${{ matrix.go-version }} | sed -e s/.x//)" make binaries
test-integration-docker-compatibility:
- runs-on: ubuntu-22.04
timeout-minutes: 30
+ name: docker
+ runs-on: ubuntu-24.04
steps:
- - uses: actions/checkout@v3.5.3
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 1
- - uses: actions/setup-go@v4
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with:
go-version: ${{ env.GO_VERSION }}
- cache: true
check-latest: true
- name: "Register QEMU (tonistiigi/binfmt)"
- run: docker run --privileged --rm tonistiigi/binfmt --install all
+ run: |
+ # `--install all` will only install emulation for architectures that cannot be natively executed
+ # Since some arm64 platforms do provide native fallback execution for 32 bits,
+ # armv7 emulation may or may not be installed, causing variance in the result of `uname -m`.
+ # To avoid that, we explicitly list the architectures we do want emulation for.
+ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm64
+ docker run --privileged --rm tonistiigi/binfmt --install linux/arm/v7
- name: "Prepare integration test environment"
run: |
sudo apt-get install -y expect
+ go install -v gotest.tools/gotestsum@v1
- name: "Ensure that the integration test suite is compatible with Docker"
- run: go test -timeout 20m -v -exec sudo ./cmd/nerdctl/... -args -test.target=docker -test.kill-daemon
+ run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker
+ - name: "Ensure that the IPv6 integration test suite is compatible with Docker"
+ run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker -test.only-ipv6
+ - name: "Ensure that the integration test suite is compatible with Docker (flaky only)"
+ run: WITH_SUDO=true ./hack/test-integration.sh -test.target=docker -test.only-flaky
+
+ test-integration-windows:
+ timeout-minutes: 30
+ name: windows
+ runs-on: windows-2022
+ defaults:
+ run:
+ shell: bash
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
+ with:
+ go-version: ${{ env.GO_VERSION }}
+ check-latest: true
+ - run: go install ./cmd/nerdctl
+ - run: go install -v gotest.tools/gotestsum@v1
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ repository: containerd/containerd
+ ref: v1.7.25
+ path: containerd
+ fetch-depth: 1
+ - name: "Set up CNI"
+ working-directory: containerd
+ run: GOPATH=$(go env GOPATH) script/setup/install-cni-windows
+ - name: "Set up containerd"
+ env:
+ ctrdVersion: 1.7.25
+ run: powershell hack/configure-windows-ci.ps1
+ - name: "Run integration tests"
+ run: ./hack/test-integration.sh -test.only-flaky=false
+ - name: "Run integration tests (flaky)"
+ run: ./hack/test-integration.sh -test.only-flaky=true
+
+ test-integration-freebsd:
+ timeout-minutes: 30
+ name: FreeBSD
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
+ with:
+ fetch-depth: 1
+ - uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
+ with:
+ path: /root/.vagrant.d
+ key: vagrant-${{ matrix.box }}
+ - name: Set up vagrant
+ run: |
+ # from https://github.com/containerd/containerd/blob/v2.0.2/.github/workflows/ci.yml#L583-L596
+ # which is based on https://github.com/opencontainers/runc/blob/v1.1.8/.cirrus.yml#L41-L49
+ curl -fsSL https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
+ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
+ sudo sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources
+ sudo apt-get update
+ sudo apt-get install -y libvirt-daemon libvirt-daemon-system vagrant ovmf
+ # https://github.com/vagrant-libvirt/vagrant-libvirt/issues/1725#issuecomment-1454058646
+ sudo cp /usr/share/OVMF/OVMF_VARS_4M.fd /var/lib/libvirt/qemu/nvram/
+ sudo systemctl enable --now libvirtd
+ sudo apt-get build-dep -y ruby-libvirt
+ sudo apt-get install -y --no-install-recommends libxslt-dev libxml2-dev libvirt-dev ruby-bundler ruby-dev zlib1g-dev
+ sudo vagrant plugin install vagrant-libvirt
+ - name: Boot VM
+ run: |
+ ln -sf Vagrantfile.freebsd Vagrantfile
+ sudo vagrant up --no-tty
+ - name: test-unit
+ run: sudo vagrant up --provision-with=test-unit
+ - name: test-integration
+ run: sudo vagrant up --provision-with=test-integration
diff --git a/.gitignore b/.gitignore
index 646cd932d8f..9381e921112 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ _output
# vagrant
/.vagrant
+Vagrantfile
diff --git a/.golangci.yml b/.golangci.yml
index 0d2fec29133..f7dbbab93e0 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,11 +1,11 @@
---
run:
concurrency: 6
- deadline: 5m
+ timeout: 5m
linters:
disable-all: true
enable:
- - depguard
+ # - depguard
- gofmt
- goimports
- govet
@@ -134,3 +134,9 @@ linters-settings:
- typeUnparen
- unnamedResult
- unnecessaryBlock
+
+issues:
+ exclude-rules:
+ - linters:
+ - revive
+ text: "unused-parameter"
diff --git a/Dockerfile b/Dockerfile
index 074d500360a..76866106b51 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,51 +18,58 @@
# TODO: verify commit hash
# Basic deps
-ARG CONTAINERD_VERSION=v1.7.1
-ARG RUNC_VERSION=v1.1.7
-ARG CNI_PLUGINS_VERSION=v1.3.0
+ARG CONTAINERD_VERSION=v2.0.2
+ARG RUNC_VERSION=v1.2.4
+ARG CNI_PLUGINS_VERSION=v1.6.2
# Extra deps: Build
-ARG BUILDKIT_VERSION=v0.11.6
+ARG BUILDKIT_VERSION=v0.19.0
# Extra deps: Lazy-pulling
-ARG STARGZ_SNAPSHOTTER_VERSION=v0.14.3
+ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3
# Extra deps: Encryption
-ARG IMGCRYPT_VERSION=v1.1.7
+ARG IMGCRYPT_VERSION=v2.0.0
# Extra deps: Rootless
-ARG ROOTLESSKIT_VERSION=v1.1.0
-ARG SLIRP4NETNS_VERSION=v1.2.0
+ARG ROOTLESSKIT_VERSION=v2.3.2
+ARG SLIRP4NETNS_VERSION=v1.3.1
# Extra deps: bypass4netns
-ARG BYPASS4NETNS_VERSION=v0.3.0
+ARG BYPASS4NETNS_VERSION=v0.4.2
# Extra deps: FUSE-OverlayFS
-ARG FUSE_OVERLAYFS_VERSION=v1.12
-ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v1.0.6
-# Extra deps: IPFS
-ARG KUBO_VERSION=v0.20.0
+ARG FUSE_OVERLAYFS_VERSION=v1.14
+ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.1
# Extra deps: Init
ARG TINI_VERSION=v0.19.0
# Extra deps: Debug
ARG BUILDG_VERSION=v0.4.1
# Test deps
-ARG GO_VERSION=1.20
-ARG UBUNTU_VERSION=22.04
+ARG GO_VERSION=1.23
+ARG UBUNTU_VERSION=24.04
ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1
-ARG GOTESTSUM_VERSION=v1.10.0
-ARG NYDUS_VERSION=v2.2.1
+ARG GOTESTSUM_VERSION=v1.12.0
+ARG NYDUS_VERSION=v2.3.0
+ARG SOCI_SNAPSHOTTER_VERSION=0.8.0
+ARG KUBO_VERSION=v0.32.1
-FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.2.1 AS xx
+FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx
-FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bullseye AS build-base-debian
+FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base-debian
COPY --from=xx / /
ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update && \
- apt-get install -y git pkg-config dpkg-dev
+RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
+ git \
+ dpkg-dev
ARG TARGETARCH
# libbtrfs: for containerd
# libseccomp: for runc and bypass4netns
-RUN xx-apt-get update && \
- xx-apt-get install -y binutils gcc libc6-dev libbtrfs-dev libseccomp-dev
+RUN xx-apt-get update -qq && xx-apt-get install -qq --no-install-recommends \
+ binutils \
+ gcc \
+ libc6-dev \
+ libbtrfs-dev \
+ libseccomp-dev \
+ pkg-config
+RUN git config --global advice.detachedHead false
FROM build-base-debian AS build-containerd
ARG TARGETARCH
@@ -72,10 +79,7 @@ WORKDIR /go/src/github.com/containerd/containerd
RUN git checkout ${CONTAINERD_VERSION} && \
mkdir -p /out /out/$TARGETARCH && \
cp -a containerd.service /out
-ENV CGO_ENABLED=1
-ENV GO111MODULE=off
-# TODO: how to build containerd as static binaries? https://github.com/containerd/containerd/issues/6158
-RUN GO=xx-go make && \
+RUN GO=xx-go make STATIC=1 && \
cp -a bin/containerd bin/containerd-shim-runc-v2 bin/ctr /out/$TARGETARCH
FROM build-base-debian AS build-runc
@@ -86,7 +90,7 @@ WORKDIR /go/src/github.com/opencontainers/runc
RUN git checkout ${RUNC_VERSION} && \
mkdir -p /out
ENV CGO_ENABLED=1
-RUN GO=xx-go make static && \
+RUN GO=xx-go CC=$(xx-info)-gcc STRIP=$(xx-info)-strip make static && \
xx-verify --static runc && cp -v -a runc /out/runc.${TARGETARCH}
FROM build-base-debian AS build-bypass4netns
@@ -100,27 +104,33 @@ ENV CGO_ENABLED=1
RUN GO=xx-go make static && \
xx-verify --static bypass4netns && cp -a bypass4netns bypass4netnsd /out/${TARGETARCH}
+FROM build-base-debian AS build-kubo
+ARG KUBO_VERSION
+ARG TARGETARCH
+RUN git clone https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo
+WORKDIR /go/src/github.com/ipfs/kubo
+RUN git checkout ${KUBO_VERSION} && \
+ mkdir -p /out/${TARGETARCH}
+ENV CGO_ENABLED=0
+RUN xx-go --wrap && \
+ make build && \
+ xx-verify --static cmd/ipfs/ipfs && cp -a cmd/ipfs/ipfs /out/${TARGETARCH}
+
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build-base
RUN apk add --no-cache make git curl
-COPY . /go/src/github.com/containerd/nerdctl
-WORKDIR /go/src/github.com/containerd/nerdctl
+RUN git config --global advice.detachedHead false
FROM build-base AS build-minimal
RUN BINDIR=/out/bin make binaries install
# We do not set CMD to `go test` here, because it requires systemd
-FROM build-base AS build-full
+FROM build-base AS build-dependencies
ARG TARGETARCH
ENV GOARCH=${TARGETARCH}
-RUN BINDIR=/out/bin make binaries install
-WORKDIR /nowhere
COPY ./Dockerfile.d/SHA256SUMS.d/ /SHA256SUMS.d
-COPY README.md /out/share/doc/nerdctl/
-COPY docs /out/share/doc/nerdctl/docs
+WORKDIR /nowhere
RUN echo "${TARGETARCH:-amd64}" | sed -e s/amd64/x86_64/ -e s/arm64/aarch64/ | tee /target_uname_m
-RUN mkdir -p /out/share/doc/nerdctl-full && \
- echo "# nerdctl (full distribution)" > /out/share/doc/nerdctl-full/README.md && \
- echo "- nerdctl: $(cd /go/src/github.com/containerd/nerdctl && git describe --tags)" >> /out/share/doc/nerdctl-full/README.md
+RUN mkdir -p /out/share/doc/nerdctl-full && touch /out/share/doc/nerdctl-full/README.md
ARG CONTAINERD_VERSION
COPY --from=build-containerd /out/${TARGETARCH:-amd64}/* /out/bin/
COPY --from=build-containerd /out/containerd.service /out/lib/systemd/system/containerd.service
@@ -130,7 +140,7 @@ COPY --from=build-runc /out/runc.${TARGETARCH:-amd64} /out/bin/runc
RUN echo "- runc: ${RUNC_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG CNI_PLUGINS_VERSION
RUN fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VERSION}.tgz" && \
- curl -o "${fname}" -fSL "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containernetworking/plugins/releases/download/${CNI_PLUGINS_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/cni-plugins-${CNI_PLUGINS_VERSION}" | sha256sum -c && \
mkdir -p /out/libexec/cni && \
tar xzf "${fname}" -C /out/libexec/cni && \
@@ -138,10 +148,11 @@ RUN fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VER
echo "- CNI plugins: ${CNI_PLUGINS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG BUILDKIT_VERSION
RUN fname="buildkit-${BUILDKIT_VERSION}.${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/moby/buildkit/releases/download/${BUILDKIT_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/buildkit-${BUILDKIT_VERSION}" | sha256sum -c && \
tar xzf "${fname}" -C /out && \
- rm -f "${fname}" /out/bin/buildkit-qemu-* /out/bin/buildkit-runc && \
+ rm -f "${fname}" /out/bin/buildkit-qemu-* /out/bin/buildkit-cni-* /out/bin/buildkit-runc && \
+ for f in /out/libexec/cni/*; do ln -s ../libexec/cni/$(basename $f) /out/bin/buildkit-cni-$(basename $f); done && \
echo "- BuildKit: ${BUILDKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md
# NOTE: github.com/moby/buildkit/examples/systemd is not included in BuildKit v0.8.x, will be included in v0.9.x
RUN cd /out/lib/systemd/system && \
@@ -151,8 +162,8 @@ RUN cd /out/lib/systemd/system && \
echo "# This file was converted from containerd.service, with \`sed -E '${sedcomm}'\`" >> buildkit.service
ARG STARGZ_SNAPSHOTTER_VERSION
RUN fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \
- curl -o "stargz-snapshotter.service" -fSL "https://raw.githubusercontent.com/containerd/stargz-snapshotter/${STARGZ_SNAPSHOTTER_VERSION}/script/config/etc/systemd/system/stargz-snapshotter.service" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/stargz-snapshotter/releases/download/${STARGZ_SNAPSHOTTER_VERSION}/${fname}" && \
+ curl -o "stargz-snapshotter.service" -fsSL --proto '=https' --tlsv1.2 "https://raw.githubusercontent.com/containerd/stargz-snapshotter/${STARGZ_SNAPSHOTTER_VERSION}/script/config/etc/systemd/system/stargz-snapshotter.service" && \
grep "${fname}" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \
grep "stargz-snapshotter.service" "/SHA256SUMS.d/stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}" | sha256sum -c - && \
tar xzf "${fname}" -C /out/bin && \
@@ -162,18 +173,12 @@ RUN fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-$
ARG IMGCRYPT_VERSION
RUN git clone https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \
cd /go/src/github.com/containerd/imgcrypt && \
+ git checkout "${IMGCRYPT_VERSION}" && \
CGO_ENABLED=0 make && DESTDIR=/out make install && \
echo "- imgcrypt: ${IMGCRYPT_VERSION}" >> /out/share/doc/nerdctl-full/README.md
-ARG ROOTLESSKIT_VERSION
-RUN fname="rootlesskit-$(cat /target_uname_m).tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \
- grep "${fname}" "/SHA256SUMS.d/rootlesskit-${ROOTLESSKIT_VERSION}" | sha256sum -c && \
- tar xzf "${fname}" -C /out/bin && \
- rm -f "${fname}" /out/bin/rootlesskit-docker-proxy && \
- echo "- RootlessKit: ${ROOTLESSKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG SLIRP4NETNS_VERSION
RUN fname="slirp4netns-$(cat /target_uname_m)" && \
- curl -o "${fname}" -fSL "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/slirp4netns/releases/download/${SLIRP4NETNS_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/slirp4netns-${SLIRP4NETNS_VERSION}" | sha256sum -c && \
mv "${fname}" /out/bin/slirp4netns && \
chmod +x /out/bin/slirp4netns && \
@@ -183,45 +188,43 @@ COPY --from=build-bypass4netns /out/${TARGETARCH:-amd64}/* /out/bin/
RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG FUSE_OVERLAYFS_VERSION
RUN fname="fuse-overlayfs-$(cat /target_uname_m)" && \
- curl -o "${fname}" -fSL "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containers/fuse-overlayfs/releases/download/${FUSE_OVERLAYFS_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/fuse-overlayfs-${FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \
mv "${fname}" /out/bin/fuse-overlayfs && \
chmod +x /out/bin/fuse-overlayfs && \
echo "- fuse-overlayfs: ${FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG CONTAINERD_FUSE_OVERLAYFS_VERSION
RUN fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION/v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/containerd/fuse-overlayfs-snapshotter/releases/download/${CONTAINERD_FUSE_OVERLAYFS_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION}" | sha256sum -c && \
tar xzf "${fname}" -C /out/bin && \
rm -f "${fname}" && \
echo "- containerd-fuse-overlayfs: ${CONTAINERD_FUSE_OVERLAYFS_VERSION}" >> /out/share/doc/nerdctl-full/README.md
-ARG KUBO_VERSION
-RUN fname="kubo_${KUBO_VERSION}_${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/ipfs/kubo/releases/download/${KUBO_VERSION}/${fname}" && \
- grep "${fname}" "/SHA256SUMS.d/kubo-${KUBO_VERSION}" | sha256sum -c && \
- tmpout=$(mktemp -d) && \
- tar -C ${tmpout} -xzf "${fname}" kubo/ipfs && \
- mv ${tmpout}/kubo/ipfs /out/bin/ && \
- echo "- Kubo (IPFS): ${KUBO_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG TINI_VERSION
RUN fname="tini-static-${TARGETARCH:-amd64}" && \
- curl -o "${fname}" -fSL "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/tini-${TINI_VERSION}" | sha256sum -c && \
cp -a "${fname}" /out/bin/tini && chmod +x /out/bin/tini && \
echo "- Tini: ${TINI_VERSION}" >> /out/share/doc/nerdctl-full/README.md
ARG BUILDG_VERSION
RUN fname="buildg-${BUILDG_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
- curl -o "${fname}" -fSL "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/ktock/buildg/releases/download/${BUILDG_VERSION}/${fname}" && \
grep "${fname}" "/SHA256SUMS.d/buildg-${BUILDG_VERSION}" | sha256sum -c && \
tar xzf "${fname}" -C /out/bin && \
rm -f "${fname}" && \
echo "- buildg: ${BUILDG_VERSION}" >> /out/share/doc/nerdctl-full/README.md
+ARG ROOTLESSKIT_VERSION
+RUN fname="rootlesskit-$(cat /target_uname_m).tar.gz" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/rootless-containers/rootlesskit/releases/download/${ROOTLESSKIT_VERSION}/${fname}" && \
+ grep "${fname}" "/SHA256SUMS.d/rootlesskit-${ROOTLESSKIT_VERSION}" | sha256sum -c && \
+ tar xzf "${fname}" -C /out/bin && \
+ rm -f "${fname}" /out/bin/rootlesskit-docker-proxy && \
+ echo "- RootlessKit: ${ROOTLESSKIT_VERSION}" >> /out/share/doc/nerdctl-full/README.md
RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \
echo "## License" >> /out/share/doc/nerdctl-full/README.md && \
echo "- bin/slirp4netns: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/rootless-containers/slirp4netns/blob/${SLIRP4NETNS_VERSION}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \
echo "- bin/fuse-overlayfs: [GNU GENERAL PUBLIC LICENSE, Version 2](https://github.com/containers/fuse-overlayfs/blob/${FUSE_OVERLAYFS_VERSION}/COPYING)" >> /out/share/doc/nerdctl-full/README.md && \
- echo "- bin/ipfs: [Combination of MIT-only license and dual MIT/Apache-2.0 license](https://github.com/ipfs/kubo/blob/${KUBO_VERSION}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \
echo "- bin/{runc,bypass4netns,bypass4netnsd}: Apache License 2.0, statically linked with libseccomp ([LGPL 2.1](https://github.com/seccomp/libseccomp/blob/main/LICENSE), source code available at https://github.com/seccomp/libseccomp/)" >> /out/share/doc/nerdctl-full/README.md && \
echo "- bin/tini: [MIT License](https://github.com/krallin/tini/blob/${TINI_VERSION}/LICENSE)" >> /out/share/doc/nerdctl-full/README.md && \
echo "- Other files: [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)" >> /out/share/doc/nerdctl-full/README.md && \
@@ -229,13 +232,20 @@ RUN echo "" >> /out/share/doc/nerdctl-full/README.md && \
mv /tmp/SHA256SUMS /out/share/doc/nerdctl-full/SHA256SUMS && \
chown -R 0:0 /out
+FROM build-dependencies AS build-full
+COPY . /go/src/github.com/containerd/nerdctl
+RUN { echo "# nerdctl (full distribution)"; echo "- nerdctl: $(cd /go/src/github.com/containerd/nerdctl && git describe --tags)"; cat /out/share/doc/nerdctl-full/README.md; } > /out/share/doc/nerdctl-full/README.md.new; mv /out/share/doc/nerdctl-full/README.md.new /out/share/doc/nerdctl-full/README.md
+WORKDIR /go/src/github.com/containerd/nerdctl
+RUN BINDIR=/out/bin make binaries install
+COPY README.md /out/share/doc/nerdctl/
+COPY docs /out/share/doc/nerdctl/docs
+
FROM scratch AS out-full
COPY --from=build-full /out /
FROM ubuntu:${UBUNTU_VERSION} AS base
# fuse3 is required by stargz snapshotter
-RUN apt-get update && \
- apt-get install -qq -y --no-install-recommends \
+RUN apt-get update -qq && apt-get install -qq -y --no-install-recommends \
apparmor \
bash-completion \
ca-certificates curl \
@@ -243,7 +253,7 @@ RUN apt-get update && \
dbus dbus-user-session systemd systemd-sysv \
fuse3
ARG CONTAINERIZED_SYSTEMD_VERSION
-RUN curl -L -o /docker-entrypoint.sh https://raw.githubusercontent.com/AkihiroSuda/containerized-systemd/${CONTAINERIZED_SYSTEMD_VERSION}/docker-entrypoint.sh && \
+RUN curl -o /docker-entrypoint.sh -fsSL --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/AkihiroSuda/containerized-systemd/${CONTAINERIZED_SYSTEMD_VERSION}/docker-entrypoint.sh && \
chmod +x /docker-entrypoint.sh
COPY --from=out-full / /usr/local/
RUN perl -pi -e 's/multi-user.target/docker-entrypoint.target/g' /usr/local/lib/systemd/system/*.service && \
@@ -267,12 +277,13 @@ RUN go env GOVERSION > /GOVERSION
FROM base AS test-integration
ARG DEBIAN_FRONTEND=noninteractive
# `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing
-RUN apt-get update && \
- apt-get install -qq -y \
- expect git
+RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
+ expect \
+ git \
+ make
COPY --from=goversion /GOVERSION /GOVERSION
ARG TARGETARCH
-RUN curl -L https://golang.org/dl/$(cat /GOVERSION).linux-${TARGETARCH:-amd64}.tar.gz | tar xzvC /usr/local
+RUN curl -fsSL --proto '=https' --tlsv1.2 https://golang.org/dl/$(cat /GOVERSION).linux-${TARGETARCH:-amd64}.tar.gz | tar xzvC /usr/local
ENV PATH=/usr/local/go/bin:$PATH
ARG GOTESTSUM_VERSION
RUN GOBIN=/usr/local/bin go install gotest.tools/gotestsum@${GOTESTSUM_VERSION}
@@ -281,35 +292,43 @@ WORKDIR /go/src/github.com/containerd/nerdctl
VOLUME /tmp
ENV CGO_ENABLED=0
# copy cosign binary for integration test
-COPY --from=gcr.io/projectsigstore/cosign:v2.0.0@sha256:728944a9542a7235b4358c4ab2bcea855840e9d4b9594febca5c2207f5da7f38 /ko-app/cosign /usr/local/bin/cosign
+COPY --from=ghcr.io/sigstore/cosign/cosign:v2.2.3@sha256:8fc9cad121611e8479f65f79f2e5bea58949e8a87ffac2a42cb99cf0ff079ba7 /ko-app/cosign /usr/local/bin/cosign
+# installing soci for integration test
+ARG SOCI_SNAPSHOTTER_VERSION
+RUN fname="soci-snapshotter-${SOCI_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \
+ curl -o "${fname}" -fsSL --proto '=https' --tlsv1.2 "https://github.com/awslabs/soci-snapshotter/releases/download/v${SOCI_SNAPSHOTTER_VERSION}/${fname}" && \
+ tar -C /usr/local/bin -xvf "${fname}" soci soci-snapshotter-grpc
# enable offline ipfs for integration test
+COPY --from=build-kubo /out/${TARGETARCH:-amd64}/* /usr/local/bin/
COPY ./Dockerfile.d/test-integration-etc_containerd-stargz-grpc_config.toml /etc/containerd-stargz-grpc/config.toml
COPY ./Dockerfile.d/test-integration-ipfs-offline.service /usr/local/lib/systemd/system/
COPY ./Dockerfile.d/test-integration-buildkit-nerdctl-test.service /usr/local/lib/systemd/system/
+COPY ./Dockerfile.d/test-integration-soci-snapshotter.service /usr/local/lib/systemd/system/
RUN cp /usr/local/bin/tini /usr/local/bin/tini-custom
+# using test integration containerd config
+COPY ./Dockerfile.d/test-integration-etc_containerd_config.toml /etc/containerd/config.toml
# install ipfs service. avoid using 5001(api)/8080(gateway) which are reserved by tests.
-RUN systemctl enable test-integration-ipfs-offline test-integration-buildkit-nerdctl-test && \
+RUN systemctl enable test-integration-ipfs-offline test-integration-buildkit-nerdctl-test test-integration-soci-snapshotter && \
ipfs init && \
ipfs config Addresses.API "/ip4/127.0.0.1/tcp/5888" && \
ipfs config Addresses.Gateway "/ip4/127.0.0.1/tcp/5889"
# install nydus components
ARG NYDUS_VERSION
-RUN curl -L -o nydus-static.tgz "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \
+RUN curl -o nydus-static.tgz -fsSL --proto '=https' --tlsv1.2 "https://github.com/dragonflyoss/image-service/releases/download/${NYDUS_VERSION}/nydus-static-${NYDUS_VERSION}-linux-${TARGETARCH}.tgz" && \
tar xzf nydus-static.tgz && \
mv nydus-static/nydus-image nydus-static/nydusd nydus-static/nydusify /usr/bin/ && \
rm nydus-static.tgz
-CMD ["gotestsum", "--format=testname", "--rerun-fails=2", "--packages=github.com/containerd/nerdctl/cmd/nerdctl/...", \
- "--", "-timeout=30m", "-args", "-test.kill-daemon"]
+CMD ["./hack/test-integration.sh"]
FROM test-integration AS test-integration-rootless
# Install SSH for creating systemd user session.
# (`sudo` does not work for this purpose,
# OTOH `machinectl shell` can create the session but does not propagate exit code)
-RUN apt-get update && \
- apt-get install -qq -y \
+RUN apt-get update -qq && apt-get install -qq --no-install-recommends \
uidmap \
- openssh-server openssh-client
-# TODO: update containerized-systemd to enable sshd by default, or allow `systemctl wants sshd` here
+ openssh-server \
+ openssh-client
+# TODO: update containerized-systemd to enable sshd by default, or allow `systemctl wants ssh` here
RUN ssh-keygen -q -t rsa -f /root/.ssh/id_rsa -N '' && \
useradd -m -s /bin/bash rootless && \
mkdir -p -m 0700 /home/rootless/.ssh && \
@@ -320,12 +339,9 @@ COPY ./Dockerfile.d/etc_systemd_system_user@.service.d_delegate.conf /etc/system
# ipfs daemon for rootless containerd will be enabled in /test-integration-rootless.sh
RUN systemctl disable test-integration-ipfs-offline
VOLUME /home/rootless/.local/share
-RUN go test -o /usr/local/bin/nerdctl.test -c ./cmd/nerdctl
COPY ./Dockerfile.d/test-integration-rootless.sh /
-CMD ["/test-integration-rootless.sh", \
- "gotestsum", "--format=testname", "--rerun-fails=2", "--raw-command", \
- "--", "/usr/local/go/bin/go", "tool", "test2json", "-t", "-p", "github.com/containerd/nerdctl/cmd/nerdctl", \
- "/usr/local/bin/nerdctl.test", "-test.v", "-test.timeout=30m", "-test.kill-daemon"]
+RUN chmod a+rx /test-integration-rootless.sh
+CMD ["/test-integration-rootless.sh", "./hack/test-integration.sh"]
# test for CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns
FROM test-integration-rootless AS test-integration-rootless-port-slirp4netns
diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.11.6 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.11.6
deleted file mode 100644
index ca3b6f2f90d..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.11.6
+++ /dev/null
@@ -1,2 +0,0 @@
-3f66f5bfbe509aadf1c21a26acfa472fe4c19046aa00a2d59b99733da867cd76 buildkit-v0.11.6.linux-amd64.tar.gz
-82b7452ffea166d3ef445597f9dbe3fa57c4d651e51ca7a9a581199116905524 buildkit-v0.11.6.linux-arm64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.19.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.19.0
new file mode 100644
index 00000000000..440c6b431ea
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.19.0
@@ -0,0 +1,2 @@
+9993fdd8b454e541ac14a1adf4bf53d271dbc8f3aafde45894bf689604a0a5cf buildkit-v0.19.0.linux-amd64.tar.gz
+be7f7922d8f5eea02704cd707fb62b5a18e272452243804601b523ae6bef0ef5 buildkit-v0.19.0.linux-arm64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.3.0 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.3.0
deleted file mode 100644
index 202f83df0ab..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.3.0
+++ /dev/null
@@ -1,2 +0,0 @@
-754a71ed60a4bd08726c3af705a7d55ee3df03122b12e389fdba4bea35d7dd7e cni-plugins-linux-amd64-v1.3.0.tgz
-de7a666fd6ad83a228086bd55756db62ef335a193d1b143d910b69f079e30598 cni-plugins-linux-arm64-v1.3.0.tgz
diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2
new file mode 100644
index 00000000000..109168fb84f
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2
@@ -0,0 +1,2 @@
+b8e811578fb66023f90d2e238d80cec3bdfca4b44049af74c374d4fae0f9c090 cni-plugins-linux-amd64-v1.6.2.tgz
+01e0e22acc7f7004e4588c1fe1871cc86d7ab562cd858e1761c4641d89ebfaa4 cni-plugins-linux-arm64-v1.6.2.tgz
diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v1.0.6 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v1.0.6
deleted file mode 100644
index f5731f42f9d..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v1.0.6
+++ /dev/null
@@ -1,6 +0,0 @@
-2fbf5532021a558a965358e765c21b4dbe891e00ac68d18ab088d3cea4a41613 containerd-fuse-overlayfs-1.0.6-linux-amd64.tar.gz
-9d983f341b7fb980bb7728851aaab41f706cbc55387940800fabcd7f01b33f24 containerd-fuse-overlayfs-1.0.6-linux-arm-v7.tar.gz
-27c40aea2dc37fc59dae790c87193190a8360a310fa8df16fcbec165a46d6163 containerd-fuse-overlayfs-1.0.6-linux-arm64.tar.gz
-ad92ee6c7ecbf7b1f2b78f13eccf5ca82fc5349ef93d9116bef49b6767fbc146 containerd-fuse-overlayfs-1.0.6-linux-ppc64le.tar.gz
-971243ff3da618dbdcc28ef243ed08e834f7acfc164feebd7d25502938d5cae8 containerd-fuse-overlayfs-1.0.6-linux-riscv64.tar.gz
-691c07478f009858cbba66300a0da689fe60daf7fbc13609759ff8ef14ac2a97 containerd-fuse-overlayfs-1.0.6-linux-s390x.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1
new file mode 100644
index 00000000000..6596447644d
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1
@@ -0,0 +1,6 @@
+2061a4064d163544f69e36fe56d008ab90f791906d5a96bddf87d3151fdde836 containerd-fuse-overlayfs-2.1.1-linux-amd64.tar.gz
+99d08b0f41ede108f36efb9b5d8e0613be69336785cf97a73074487b52d9e71e containerd-fuse-overlayfs-2.1.1-linux-arm-v7.tar.gz
+2219bf91d943480ce7021d6fce956379050757a500d36540b4372d45616c74eb containerd-fuse-overlayfs-2.1.1-linux-arm64.tar.gz
+a2515f00553334b23470d52b088e49c3aa69aa9d66163dc14f188684bc8c774d containerd-fuse-overlayfs-2.1.1-linux-ppc64le.tar.gz
+ae0fc07af2d34fb4c599364f82570ec43fed07f1892e493726f5414ecf8c8908 containerd-fuse-overlayfs-2.1.1-linux-riscv64.tar.gz
+1200244a100b2433cc98a7ec8a0138073e9ad1c5e11ed503f5d2b3063dd40197 containerd-fuse-overlayfs-2.1.1-linux-s390x.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.12 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.12
deleted file mode 100644
index 5da63cd7e47..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.12
+++ /dev/null
@@ -1,6 +0,0 @@
-6d2813904de47350adf6d61998c97d5c262fc01b4cf4a0b70be21097aa2acda4 fuse-overlayfs-aarch64
-357763baab2e4cfd7de07e0683252d5ea0dde5e5c7bd8aba99662e25f5e63329 fuse-overlayfs-armv7l
-5b781adc8861f095719a3d61d936621f8a17bf68cc78c73c0e693dafb679831f fuse-overlayfs-ppc64le
-8b147b181c068c858bb8e8c4966817f3f884d9aad6ee7860f2a46e82c70d02ba fuse-overlayfs-riscv64
-e3fabc7529071e490b0a5e4848feaf3a0d984b50ebe84f0c4e4686503e1d5f07 fuse-overlayfs-s390x
-152318f61b1fbe91ddaef42ed31f4a173d288af1376484bc54aff892c53f4a90 fuse-overlayfs-x86_64
diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14
new file mode 100644
index 00000000000..4ef7dca0da1
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14
@@ -0,0 +1,2 @@
+bf2c19b80e68afe1f53bae7a08cc9e7fb2f1b49bfdb9e5b49ab87cbe80b97cd1 fuse-overlayfs-aarch64
+4817a8896a9e6f0433080f88f5b71dec931e8829a89d64c71af94b0630ccb4a9 fuse-overlayfs-x86_64
diff --git a/Dockerfile.d/SHA256SUMS.d/kubo-v0.20.0 b/Dockerfile.d/SHA256SUMS.d/kubo-v0.20.0
deleted file mode 100644
index f98990e14a3..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/kubo-v0.20.0
+++ /dev/null
@@ -1,3 +0,0 @@
-# From https://github.com/ipfs/kubo/releases
-46f3f14d75640dfedb0ee79ccb87eefa30da5c00b0af2c30f02f13454f5ab072 kubo_v0.20.0_linux-amd64.tar.gz
-a073d4e9eefd5f7c1ee24f2c9d0a8975591beea34c49d8fee4c0ea731393ef84 kubo_v0.20.0_linux-arm64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.0
deleted file mode 100644
index 0a944324c85..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.0
+++ /dev/null
@@ -1,6 +0,0 @@
-3e5e0dd91f53fda3591098de368727297b56ce86ce4c121fac483251b72069bd rootlesskit-aarch64.tar.gz
-d4faf36efa88d2b9ea7f2506873fc3562e8d030da18106e9c8eb6d84d4b3edc8 rootlesskit-armv7l.tar.gz
-e47a019fdfa043ddcff8aee2593383c66935874e8328d7a8138966a28106b70f rootlesskit-ppc64le.tar.gz
-9f15b93ff7b991705db5eafe769405393ecee51c7adb553f9c255c01cfb92d9a rootlesskit-riscv64.tar.gz
-0072436fb718c79b3bea02b3d6003939a9f4cb6d0bedf3d31f14c8b4456a6b49 rootlesskit-s390x.tar.gz
-503257d14960ff04b5c90c14a4f4f3410244a1468c144430251c87971ac714d4 rootlesskit-x86_64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.1 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.1
new file mode 100644
index 00000000000..73980f90118
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v1.1.1
@@ -0,0 +1,6 @@
+b74c577abd6ad721e0b7e10a74f4c5ac26cb3afe005ad3d28d4d7912c356079f rootlesskit-aarch64.tar.gz
+95c27e6808c942c67ab93d94e37bada3a62cfc47de848101889f8e3ba5c9f7dd rootlesskit-armv7l.tar.gz
+df35c74cd030e1b3978f28d1cb7c909da2ab962fb0c9369463d43a89b9f16cc2 rootlesskit-ppc64le.tar.gz
+79af3e96e9d6deddc5faa4680de7e28120ae333386c48a30e79fe156f17bad9b rootlesskit-riscv64.tar.gz
+32da9a11b67340ff498de8a3268673277a1e1d9e9d8d5f619bbf09305beaaa6c rootlesskit-s390x.tar.gz
+3c83affbb405cafe2d32e2e24462af9b4dcfa19e3809030012ad0d4e3fd49e8f rootlesskit-x86_64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.2 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.2
new file mode 100644
index 00000000000..9c7f6e338d0
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.2
@@ -0,0 +1,6 @@
+0a4ed18c6794bfe5821cc6548f52b26b3b2296170f05df194c2073545200d968 rootlesskit-aarch64.tar.gz
+f9faaf3b91e02764eb8308c7a7da7de55311de9fea665ca1b2632421b9286bcc rootlesskit-armv7l.tar.gz
+fc1120af52071dc2a6984eec8dfc74f5d973fb28fa6c6d7ec77523a636ebf641 rootlesskit-ppc64le.tar.gz
+acc1d39483df101bfa204c8dbf61d2e7ac85b246b3e9c606dabeaaeededd0130 rootlesskit-riscv64.tar.gz
+9e1c29b1b82162a71d435edf80898adcb5a41c0ec51202c36856276689eb7b52 rootlesskit-s390x.tar.gz
+5d402d7995f1e2c369240de3c6f8eb4cc2a3d1f0f4877ac5362044b2e83962e9 rootlesskit-x86_64.tar.gz
diff --git a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.2.0 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.2.0
deleted file mode 100644
index 77c7008dec2..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.2.0
+++ /dev/null
@@ -1,6 +0,0 @@
-fb82c4a8e63fe1acc0bdc92300080b8c80e514e409dc70fd593045df644ac306 slirp4netns-aarch64
-31e2555c320746083c11c959f88313b5ac277a79a804b8c59c4b030b2ba8f41b slirp4netns-armv7l
-dcae54bc1ab854bb71aa98c9ec2ad86f6817acaa75910ccf7d899219fa926cea slirp4netns-ppc64le
-2d420e3e1ab51526d9e05ce0020a505775ffcf00f64d5244973a0e0602d8c864 slirp4netns-riscv64
-65925c8d6bea7bd380a6271b06ca63dab425599ab759eb055072cff9d7f33fb9 slirp4netns-s390x
-11080fdfb2c47b99f2b0c2b72d92cc64400d0eaba11c1ec34f779e17e8844360 slirp4netns-x86_64
diff --git a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1
new file mode 100644
index 00000000000..4d0d9ea9444
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1
@@ -0,0 +1,6 @@
+2dd9aac6c2e3203e53cb7b6e4b9fc7123e4e4a9716c8bb1d95951853059a6af5 slirp4netns-aarch64
+ed618c0f2c74014bb736e9e427e18c8791ad9d68311872a41b06fac0d7cb9ef2 slirp4netns-armv7l
+a10f70209cee0dd0532fea0e8b6bfde5d16dec5206fd4b3387d861721456de66 slirp4netns-ppc64le
+38209015c2f3f4619d9fc46610852887910f33c7a0b96f7d2aa835a7bbc73f31 slirp4netns-riscv64
+9f42718455b1f9cf4b6f0efee314b78e860b8c36dbbb6290f09c8fbedda9ff8a slirp4netns-s390x
+4bc5d6c311f9fa7ae00ce54aefe10c2afaf0800fe9e99f32616a964ed804a9e1 slirp4netns-x86_64
diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.14.3 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.14.3
deleted file mode 100644
index f0a635ee028..00000000000
--- a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.14.3
+++ /dev/null
@@ -1,3 +0,0 @@
-3f5c268eac6c68f1caeb433321a07e6332ed304e9b3ae428f183982ce6df91d6 stargz-snapshotter-v0.14.3-linux-amd64.tar.gz
-5788407890786210ea9552a62fd653cea3acc48ffd354f66e5821967128d06d9 stargz-snapshotter-v0.14.3-linux-arm64.tar.gz
-f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service
diff --git a/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3 b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3
new file mode 100644
index 00000000000..e9b2bfa457c
--- /dev/null
+++ b/Dockerfile.d/SHA256SUMS.d/stargz-snapshotter-v0.16.3
@@ -0,0 +1,3 @@
+516984d13e10396f7f6090c51e4e42cc1af9a0d4b16aa81837bcdb1d5a5608d6 stargz-snapshotter-v0.16.3-linux-amd64.tar.gz
+d3ac8215603cfd002901c88c568ff5c0685d6953c012fa6ff709deb50f90b023 stargz-snapshotter-v0.16.3-linux-arm64.tar.gz
+f1cf855870af16a653d8acb9daa3edf84687c2c05323cb958f078fb148af3eec stargz-snapshotter.service
diff --git a/Dockerfile.d/test-integration-etc_containerd_config.toml b/Dockerfile.d/test-integration-etc_containerd_config.toml
new file mode 100644
index 00000000000..d37df58da75
--- /dev/null
+++ b/Dockerfile.d/test-integration-etc_containerd_config.toml
@@ -0,0 +1,12 @@
+version = 2
+
+# Enable stargz snapshotter
+[proxy_plugins]
+ [proxy_plugins.stargz]
+ type = "snapshot"
+ address = "/run/containerd-stargz-grpc/containerd-stargz-grpc.sock"
+
+# Enable soci snapshotter
+ [proxy_plugins.soci]
+ type = "snapshot"
+ address = "/run/soci-snapshotter-grpc/soci-snapshotter-grpc.sock"
diff --git a/Dockerfile.d/test-integration-ipfs-offline.service b/Dockerfile.d/test-integration-ipfs-offline.service
index cd16bdcbadf..af0662250c5 100644
--- a/Dockerfile.d/test-integration-ipfs-offline.service
+++ b/Dockerfile.d/test-integration-ipfs-offline.service
@@ -3,6 +3,7 @@ Description=ipfs daemon for integration test (offline)
[Service]
ExecStart=ipfs daemon --init --offline
+Environment=IPFS_PATH="%h/.ipfs"
[Install]
WantedBy=docker-entrypoint.target
diff --git a/Dockerfile.d/test-integration-rootless.sh b/Dockerfile.d/test-integration-rootless.sh
index 6231f34f4d6..4bdbf0fa4eb 100755
--- a/Dockerfile.d/test-integration-rootless.sh
+++ b/Dockerfile.d/test-integration-rootless.sh
@@ -21,13 +21,13 @@ if [[ "$(id -u)" = "0" ]]; then
nerdctl apparmor load
fi
- : "${WORKAROUND_CIRRUS:=}"
- if [[ "$WORKAROUND_CIRRUS" = "1" ]]; then
- touch /workaround-cirrus
+ : "${WORKAROUND_ISSUE_622:=}"
+ if [[ "$WORKAROUND_ISSUE_622" = "1" ]]; then
+ touch /workaround-issue-622
fi
# Switch to the rootless user via SSH
- systemctl start sshd
+ systemctl start ssh
exec ssh -o StrictHostKeyChecking=no rootless@localhost "$0" "$@"
else
containerd-rootless-setuptool.sh install
@@ -35,8 +35,8 @@ else
containerd-rootless-setuptool.sh nsenter -- sh -euc 'echo "options use-vc" >>/etc/resolv.conf'
fi
- if [[ -e /workaround-cirrus ]]; then
- echo "WORKAROUND_CIRRUS: Not enabling BuildKit (https://github.com/containerd/nerdctl/issues/622)" >&2
+ if [[ -e /workaround-issue-622 ]]; then
+ echo "WORKAROUND_ISSUE_622: Not enabling BuildKit (https://github.com/containerd/nerdctl/issues/622)" >&2
else
CONTAINERD_NAMESPACE="nerdctl-test" containerd-rootless-setuptool.sh install-buildkit-containerd
fi
@@ -48,7 +48,7 @@ else
[proxy_plugins]
[proxy_plugins."stargz"]
type = "snapshot"
- address = "/run/user/1000/containerd-stargz-grpc/containerd-stargz-grpc.sock"
+ address = "/run/user/$(id -u)/containerd-stargz-grpc/containerd-stargz-grpc.sock"
EOF
systemctl --user restart containerd.service
containerd-rootless-setuptool.sh -- install-ipfs --init --offline # offline ipfs daemon for testing
@@ -56,5 +56,8 @@ EOF
systemctl --user restart stargz-snapshotter.service
export IPFS_PATH="/home/rootless/.local/share/ipfs"
containerd-rootless-setuptool.sh install-bypass4netnsd
- exec "$@"
+ # Once ssh-ed, we lost the Dockerfile working dir, so, get back in the nerdctl checkout
+ cd /go/src/github.com/containerd/nerdctl
+ # We also lose the PATH (and SendEnv=PATH would require sshd config changes)
+ exec env PATH="/usr/local/go/bin:$PATH" "$@"
fi
diff --git a/Dockerfile.d/test-integration-soci-snapshotter.service b/Dockerfile.d/test-integration-soci-snapshotter.service
new file mode 100644
index 00000000000..5964702ac6a
--- /dev/null
+++ b/Dockerfile.d/test-integration-soci-snapshotter.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=soci snapshotter containerd plugin for integration test
+Documentation=https://github.com/awslabs/soci-snapshotter
+After=network.target
+Before=containerd.service
+
+[Service]
+Type=notify
+ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/soci-snapshotter-grpc && mount -t tmpfs none /var/lib/soci-snapshotter-grpc'
+ExecStart=/usr/local/bin/soci-snapshotter-grpc
+Restart=always
+RestartSec=5
+
+[Install]
+WantedBy=docker-entrypoint.target
diff --git a/EMERITUS.md b/EMERITUS.md
new file mode 100644
index 00000000000..ed17a87f02f
--- /dev/null
+++ b/EMERITUS.md
@@ -0,0 +1,21 @@
+See [`MAINTAINERS`](./MAINTAINERS) for the current active maintainers.
+- - -
+# nerdctl Emeritus Maintainers
+
+## Committers
+### Ye Sijun ([@junnplus](https://github.com/junnplus))
+Ye Sijun (GitHub ID [@junnplus](https://github.com/junnplus)) served as
+a Committer of nerdctl from November 2022 to June 2024.
+Prior to his role as a Committer, Sijun served as a Reviewer since February 2022.
+
+Sijun has made [significant improvements](https://github.com/containerd/nerdctl/pulls?q=author%3Ajunnplus+)
+especially to `nerdctl compose`, IPAM, and cosign integration.
+
+## Reviewers
+### Hanchin Hsieh ([@yuchanns](https://github.com/yuchanns))
+Hanchin Hsieh (GitHub ID [@yuchanns](https://github.com/yuchanns)) served as
+a Reviewer of nerdctl from November 2022 to June 2024.
+
+Hanchin has made significant contributions such as the addition of
+[syslog driver](https://github.com/containerd/nerdctl/pull/1377) and
+[IPv6 networking](https://github.com/containerd/nerdctl/pull/1558).
diff --git a/MAINTAINERS b/MAINTAINERS
index 1bdc6f9e2cf..8fbc21ebdf6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15,11 +15,13 @@
"ktock","Kohei Tokunaga","ktokunaga.mail@gmail.com",""
"fahedouch","Fahed Dorgaa","fahed.dorgaa@gmail.com","EE7A 5503 CE0D 38AC 5B95 A500 F35F F497 60A8 65FA"
"Zheaoli", "Zheao Li", "me@manjusaka.me","6E0D D9FA BAD5 AF61 D884 01EE 878F 445D 9C6C E65E"
-"junnplus","Ye Sijun","junnplus@gmail.com",""
+"djdongjin", "Jin Dong", "djdongjin95@gmail.com",""
+"yankay", "Kay Yan", "kay.yan@daocloud.io", ""
# REVIEWERS
# GitHub ID, Name, Email address, GPG fingerprint
"jsturtevant","James Sturtevant","jstur@microsoft.com",""
-"yuchanns", "Hanchin Hsieh", "me@yuchanns.xyz",""
"manugupt1", "Manu Gupta", "manugupt1@gmail.com","FCA9 504A 4118 EA5C F466 CC30 A5C3 A8F4 E7FE 9E10"
-"djdongjin", "Jin Dong", "djdongjin95@gmail.com",""
+
+# EMERITUS
+# See EMERITUS.md
diff --git a/Makefile b/Makefile
index d4a76cb6cd8..ae4e18c94f3 100644
--- a/Makefile
+++ b/Makefile
@@ -18,26 +18,37 @@
# Licensed under the Apache License, Version 2.0
# -----------------------------------------------------------------------------
+DOCKER ?= docker
GO ?= go
-GOOS ?= $(shell go env GOOS)
+GOOS ?= $(shell $(GO) env GOOS)
ifeq ($(GOOS),windows)
BIN_EXT := .exe
endif
-PACKAGE := github.com/containerd/nerdctl
-BINDIR ?= /usr/local/bin
+PACKAGE := github.com/containerd/nerdctl/v2
-VERSION ?= $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
-VERSION_TRIMMED := $(VERSION:v%=%)
-REVISION ?= $(shell git rev-parse HEAD)$(shell if ! git diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
+# distro builders might wanna override these
+PREFIX ?= /usr/local
+BINDIR ?= $(PREFIX)/bin
+DATADIR ?= $(PREFIX)/share
+DOCDIR ?= $(DATADIR)/doc
-GO_BUILD_LDFLAGS ?= -s -w
-export GO_BUILD=GO111MODULE=on CGO_ENABLED=0 GOOS=$(GOOS) $(GO) build -ldflags "$(GO_BUILD_LDFLAGS) -X $(PACKAGE)/pkg/version.Version=$(VERSION) -X $(PACKAGE)/pkg/version.Revision=$(REVISION)"
+MAKEFILE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+VERSION ?= $(shell git -C $(MAKEFILE_DIR) describe --match 'v[0-9]*' --dirty='.m' --always --tags)
+VERSION_TRIMMED := $(VERSION:v%=%)
+REVISION ?= $(shell git -C $(MAKEFILE_DIR) rev-parse HEAD)$(shell if ! git -C $(MAKEFILE_DIR) diff --no-ext-diff --quiet --exit-code; then echo .m; fi)
ifdef VERBOSE
VERBOSE_FLAG := -v
+ VERBOSE_FLAG_LONG := --verbose
endif
+GO_BUILD_LDFLAGS ?= -s -w
+GO_BUILD_FLAGS ?=
+export GO_BUILD=CGO_ENABLED=0 GOOS=$(GOOS) $(GO) -C $(MAKEFILE_DIR) build -ldflags "$(GO_BUILD_LDFLAGS) $(VERBOSE_FLAG) -X $(PACKAGE)/pkg/version.Version=$(VERSION) -X $(PACKAGE)/pkg/version.Revision=$(REVISION)"
+
+recursive_wildcard=$(wildcard $1$2) $(foreach e,$(wildcard $1*),$(call recursive_wildcard,$e/,$2))
+
all: binaries
help:
@@ -46,62 +57,87 @@ help:
@echo " * 'install' - Install binaries to system locations."
@echo " * 'binaries' - Build nerdctl."
@echo " * 'clean' - Clean artifacts."
+ @echo " * 'lint' - Run various linters."
nerdctl:
- $(GO_BUILD) $(VERBOSE_FLAG) -o $(CURDIR)/_output/nerdctl$(BIN_EXT) $(PACKAGE)/cmd/nerdctl
+ $(GO_BUILD) $(GO_BUILD_FLAGS) $(VERBOSE_FLAG) -o $(CURDIR)/_output/nerdctl$(BIN_EXT) ./cmd/nerdctl
clean:
find . -name \*~ -delete
find . -name \#\* -delete
- rm -rf _output/* vendor
+ rm -rf $(CURDIR)/_output/* $(MAKEFILE_DIR)/vendor
+
+lint: lint-go lint-imports lint-yaml lint-shell
+
+lint-go:
+ cd $(MAKEFILE_DIR) && GOOS=linux golangci-lint run $(VERBOSE_FLAG_LONG) ./... && \
+ GOOS=windows golangci-lint run $(VERBOSE_FLAG_LONG) ./... && \
+ GOOS=freebsd golangci-lint run $(VERBOSE_FLAG_LONG) ./...
+
+lint-imports:
+ cd $(MAKEFILE_DIR) && ./hack/lint-imports.sh
+
+lint-fix-imports:
+ cd $(MAKEFILE_DIR) && goimports-reviser -company-prefixes "github.com/containerd" ./...
+
+lint-yaml:
+ cd $(MAKEFILE_DIR) && yamllint .
+
+lint-shell: $(call recursive_wildcard,$(MAKEFILE_DIR)/,*.sh)
+ shellcheck -a -x $^
+
+test-unit:
+ go test -v $(MAKEFILE_DIR)/pkg/...
binaries: nerdctl
install:
install -D -m 755 $(CURDIR)/_output/nerdctl $(DESTDIR)$(BINDIR)/nerdctl
- install -D -m 755 $(CURDIR)/extras/rootless/containerd-rootless.sh $(DESTDIR)$(BINDIR)/containerd-rootless.sh
- install -D -m 755 $(CURDIR)/extras/rootless/containerd-rootless-setuptool.sh $(DESTDIR)$(BINDIR)/containerd-rootless-setuptool.sh
+ install -D -m 755 $(MAKEFILE_DIR)/extras/rootless/containerd-rootless.sh $(DESTDIR)$(BINDIR)/containerd-rootless.sh
+ install -D -m 755 $(MAKEFILE_DIR)/extras/rootless/containerd-rootless-setuptool.sh $(DESTDIR)$(BINDIR)/containerd-rootless-setuptool.sh
+ install -D -m 644 -t $(DESTDIR)$(DOCDIR)/nerdctl $(MAKEFILE_DIR)/docs/*.md
+
+# Note that these options will not work on macOS - unless you use gnu-tar instead of tar
+TAR_OWNER0_FLAGS=--owner=0 --group=0
+TAR_FLATTEN_FLAGS=--transform 's/.*\///g'
define make_artifact_full_linux
- DOCKER_BUILDKIT=1 docker build --output type=tar,dest=$(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar --target out-full --platform $(1) --build-arg GO_VERSION $(CURDIR)
+ $(DOCKER) build --output type=tar,dest=$(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar --target out-full --platform $(1) --build-arg GO_VERSION -f $(MAKEFILE_DIR)/Dockerfile $(MAKEFILE_DIR)
gzip -9 $(CURDIR)/_output/nerdctl-full-$(VERSION_TRIMMED)-linux-$(1).tar
endef
-TAR_OWNER0_FLAGS=--owner=0 --group=0
-TAR_FLATTEN_FLAGS=--transform 's/.*\///g'
-
artifacts: clean
- GOOS=linux GOARCH=amd64 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-amd64.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-amd64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=linux GOARCH=arm64 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm64.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=arm64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=linux GOARCH=arm GOARM=7 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm-v7.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=arm GOARM=7 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-arm-v7.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=linux GOARCH=ppc64le make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-ppc64le.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=ppc64le make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-ppc64le.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=linux GOARCH=riscv64 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-riscv64.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=riscv64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-riscv64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=linux GOARCH=s390x make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-s390x.tar.gz _output/nerdctl extras/rootless/*
+ GOOS=linux GOARCH=s390x make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-s390x.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/extras/rootless/*
- GOOS=windows GOARCH=amd64 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-windows-amd64.tar.gz _output/nerdctl.exe
+ GOOS=windows GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-windows-amd64.tar.gz $(CURDIR)/_output/nerdctl.exe
- GOOS=freebsd GOARCH=amd64 make -C $(CURDIR) binaries
- tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-freebsd-amd64.tar.gz _output/nerdctl
+ GOOS=freebsd GOARCH=amd64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries
+ tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-freebsd-amd64.tar.gz $(CURDIR)/_output/nerdctl
rm -f $(CURDIR)/_output/nerdctl $(CURDIR)/_output/nerdctl.exe
$(call make_artifact_full_linux,amd64)
$(call make_artifact_full_linux,arm64)
- go mod vendor
- tar $(TAR_OWNER0_FLAGS) -czf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-go-mod-vendor.tar.gz go.mod go.sum vendor
+ $(GO) -C $(MAKEFILE_DIR) mod vendor
+ tar $(TAR_OWNER0_FLAGS) -czf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-go-mod-vendor.tar.gz $(MAKEFILE_DIR)/go.mod $(MAKEFILE_DIR)/go.sum $(MAKEFILE_DIR)/vendor
.PHONY: \
help \
@@ -110,3 +146,7 @@ artifacts: clean
binaries \
install \
artifacts
+ lint \
+ lint-yaml \
+ lint-go \
+ lint-shell
diff --git a/README.md b/README.md
index 70377760f80..b0cb1698a95 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,12 @@
# nerdctl: Docker-compatible CLI for containerd
+
+
`nerdctl` is a Docker-compatible CLI for [contai**nerd**](https://containerd.io).
✅ Same UI/UX as `docker`
@@ -125,11 +131,11 @@ Binaries are available here:
In addition to containerd, the following components should be installed:
- [CNI plugins](https://github.com/containernetworking/plugins): for using `nerdctl run`.
- - v1.1.0 or later is highly recommended. Older versions require extra [CNI isolation plugin](https://github.com/AkihiroSuda/cni-isolation) for isolating bridge networks (`nerdctl network create`).
+ - v1.1.0 or later is highly recommended.
- [BuildKit](https://github.com/moby/buildkit) (OPTIONAL): for using `nerdctl build`. BuildKit daemon (`buildkitd`) needs to be running. See also [the document about setting up BuildKit](./docs/build.md).
- v0.11.0 or later is highly recommended. Some features, such as pruning caches with `nerdctl system prune`, do not work with older versions.
- [RootlessKit](https://github.com/rootless-containers/rootlesskit) and [slirp4netns](https://github.com/rootless-containers/slirp4netns) (OPTIONAL): for [Rootless mode](./docs/rootless.md)
- - RootlessKit needs to be v0.10.0 or later. v0.14.1 or later is recommended.
+ - RootlessKit needs to be v0.10.0 or later. v2.0.0 or later is recommended.
- slirp4netns needs to be v0.4.0 or later. v1.1.7 or later is recommended.
These dependencies are included in `nerdctl-full---.tar.gz`, but not included in `nerdctl---.tar.gz`.
@@ -184,14 +190,12 @@ Also, `nerdctl` might be potentially useful for debugging Kubernetes clusters, b
Major:
-- On-demand image pulling (lazy-pulling) using [Stargz](./docs/stargz.md)/[Nydus](./docs/nydus.md)/[OverlayBD](./docs/overlaybd.md) Snapshotter: `nerdctl --snapshotter=stargz|nydus|overlaybd run IMAGE` .
+- On-demand image pulling (lazy-pulling) using [Stargz](./docs/stargz.md)/[Nydus](./docs/nydus.md)/[OverlayBD](./docs/overlaybd.md)/[SOCI](./docs/soci.md) Snapshotter: `nerdctl --snapshotter=stargz|nydus|overlaybd|soci run IMAGE` .
- [Image encryption and decryption using ocicrypt (imgcrypt)](./docs/ocicrypt.md): `nerdctl image (encrypt|decrypt) SRC DST`
- [P2P image distribution using IPFS](./docs/ipfs.md): `nerdctl run ipfs://CID` .
P2P image distribution (IPFS) is completely optional. Your host is NOT connected to any P2P network, unless you opt in to [install and run IPFS daemon](https://docs.ipfs.io/install/).
-- Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too).
- Requires kernel >= 5.12, and crun >= 1.4 or runc >= 1.1 (PR [#3272](https://github.com/opencontainers/runc/pull/3272)).
- [Cosign integration](./docs/cosign.md): `nerdctl pull --verify=cosign` and `nerdctl push --sign=cosign`, and [in Compose](./docs/cosign.md#cosign-in-compose)
-- [Accelerated rootless containers using bypass4netns](./docs/rootless.md): `nerdctl run --label nerdctl/bypass4netns=true`
+- [Accelerated rootless containers using bypass4netns](./docs/rootless.md): `nerdctl run --annotation nerdctl/bypass4netns=true`
Minor:
@@ -205,11 +209,17 @@ Minor:
- Better multi-platform support, e.g., `nerdctl pull --all-platforms IMAGE`
- Applying an (existing) AppArmor profile to rootless containers: `nerdctl run --security-opt apparmor=`.
Use `sudo nerdctl apparmor load` to load the `nerdctl-default` profile.
+- Systemd compatibility support: `nerdctl run --systemd=always`
Trivial:
- Inspecting raw OCI config: `nerdctl container inspect --mode=native` .
+## Features implemented in `nerdctl` ahead of Docker
+
+- Recursive read-only (RRO) bind-mount: `nerdctl run -v /mnt:/mnt:rro` (make children such as `/mnt/usb` to be read-only, too).
+ Requires kernel >= 5.12.
+The same feature was later introduced in Docker v25 with a different syntax. nerdctl will support Docker v25 syntax too in the future.
## Similar tools
- [`ctr`](https://github.com/containerd/containerd/tree/main/cmd/ctr): incompatible with Docker CLI, and not friendly to users.
@@ -243,30 +253,11 @@ Run `make && sudo make install`.
See the header of [`go.mod`](./go.mod) for the minimum supported version of Go.
-Using `go install github.com/containerd/nerdctl/cmd/nerdctl` is possible, but unrecommended because it does not fill version strings printed in `nerdctl version`
-
-### Test suite
-
-#### Running unit tests
-
-Run `go test -v ./pkg/...`
-
-#### Running integration test suite against nerdctl
-
-Run `go test -exec sudo -v ./cmd/nerdctl/...` after `make && sudo make install`.
-
-For testing rootless mode, `-exec sudo` is not needed.
-
-To run tests in a container:
-
-```bash
-docker build -t test-integration --target test-integration .
-docker run -t --rm --privileged test-integration
-```
+Using `go install github.com/containerd/nerdctl/v2/cmd/nerdctl` is possible, but unrecommended because it does not fill version strings printed in `nerdctl version`
-#### Running integration test suite against Docker
+### Testing
-Run `go test -exec sudo -v ./cmd/nerdctl/... -args -test.target=docker` to ensure that the test suite is compatible with Docker.
+See [testing nerdctl](docs/testing/README.md).
### Contributing to nerdctl
diff --git a/Vagrantfile.freebsd b/Vagrantfile.freebsd
new file mode 100644
index 00000000000..f3a2e4a4d4a
--- /dev/null
+++ b/Vagrantfile.freebsd
@@ -0,0 +1,66 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# Copyright The containerd Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Vagrantfile for FreeBSD
+Vagrant.configure("2") do |config|
+ config.vm.box = "generic/freebsd14"
+
+ memory = 2048
+ cpus = 1
+ config.vm.provider :virtualbox do |v, o|
+ v.memory = memory
+ v.cpus = cpus
+ end
+ config.vm.provider :libvirt do |v|
+ v.memory = memory
+ v.cpus = cpus
+ end
+
+ config.vm.synced_folder ".", "/vagrant", type: "rsync"
+
+ config.vm.provision "install", type: "shell", run: "once" do |sh|
+ sh.inline = <<~SHELL
+ #!/usr/bin/env bash
+ set -eux -o pipefail
+ # `pkg install go` still installs Go 1.20 (March 2024)
+ pkg install -y go122 containerd runj
+ ln -s go122 /usr/local/bin/go
+ cd /vagrant
+ go install ./cmd/nerdctl
+ SHELL
+ end
+
+ config.vm.provision "test-unit", type: "shell", run: "never" do |sh|
+ sh.inline = <<~SHELL
+ #!/usr/bin/env bash
+ set -eux -o pipefail
+ cd /vagrant
+ go test -v ./pkg/...
+ SHELL
+ end
+
+ config.vm.provision "test-integration", type: "shell", run: "never" do |sh|
+ sh.inline = <<~SHELL
+ #!/usr/bin/env bash
+ set -eux -o pipefail
+ daemon -o containerd.out containerd
+ sleep 3
+ /root/go/bin/nerdctl run --rm --net=none dougrabson/freebsd-minimal:13 echo "Nerdctl is up and running."
+ SHELL
+ end
+
+end
diff --git a/cmd/nerdctl/apparmor_inspect_linux.go b/cmd/nerdctl/apparmor/apparmor_inspect_linux.go
similarity index 87%
rename from cmd/nerdctl/apparmor_inspect_linux.go
rename to cmd/nerdctl/apparmor/apparmor_inspect_linux.go
index 0071a22a824..acfec41a959 100644
--- a/cmd/nerdctl/apparmor_inspect_linux.go
+++ b/cmd/nerdctl/apparmor/apparmor_inspect_linux.go
@@ -14,16 +14,16 @@
limitations under the License.
*/
-package main
+package apparmor
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
-
- "github.com/containerd/nerdctl/pkg/cmd/apparmor"
- "github.com/containerd/nerdctl/pkg/defaults"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
)
func newApparmorInspectCommand() *cobra.Command {
diff --git a/cmd/nerdctl/apparmor_linux.go b/cmd/nerdctl/apparmor/apparmor_linux.go
similarity index 79%
rename from cmd/nerdctl/apparmor_linux.go
rename to cmd/nerdctl/apparmor/apparmor_linux.go
index e39985e9294..d1fa8ff977a 100644
--- a/cmd/nerdctl/apparmor_linux.go
+++ b/cmd/nerdctl/apparmor/apparmor_linux.go
@@ -14,18 +14,20 @@
limitations under the License.
*/
-package main
+package apparmor
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newApparmorCommand() *cobra.Command {
+func NewApparmorCommand() *cobra.Command {
cmd := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "apparmor",
Short: "Manage AppArmor profiles",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
diff --git a/cmd/nerdctl/apparmor/apparmor_linux_test.go b/cmd/nerdctl/apparmor/apparmor_linux_test.go
new file mode 100644
index 00000000000..2cf255871ba
--- /dev/null
+++ b/cmd/nerdctl/apparmor/apparmor_linux_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package apparmor
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/apparmor_list_linux.go b/cmd/nerdctl/apparmor/apparmor_list_linux.go
similarity index 94%
rename from cmd/nerdctl/apparmor_list_linux.go
rename to cmd/nerdctl/apparmor/apparmor_list_linux.go
index aa0073b048e..f1483e99db3 100644
--- a/cmd/nerdctl/apparmor_list_linux.go
+++ b/cmd/nerdctl/apparmor/apparmor_list_linux.go
@@ -14,12 +14,13 @@
limitations under the License.
*/
-package main
+package apparmor
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/apparmor"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor"
)
func newApparmorLsCommand() *cobra.Command {
diff --git a/cmd/nerdctl/apparmor_load_linux.go b/cmd/nerdctl/apparmor/apparmor_load_linux.go
similarity index 90%
rename from cmd/nerdctl/apparmor_load_linux.go
rename to cmd/nerdctl/apparmor/apparmor_load_linux.go
index 38b7547242b..ccd51f406d4 100644
--- a/cmd/nerdctl/apparmor_load_linux.go
+++ b/cmd/nerdctl/apparmor/apparmor_load_linux.go
@@ -14,14 +14,15 @@
limitations under the License.
*/
-package main
+package apparmor
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/cmd/apparmor"
- "github.com/containerd/nerdctl/pkg/defaults"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
)
func newApparmorLoadCommand() *cobra.Command {
diff --git a/cmd/nerdctl/apparmor_unload_linux.go b/cmd/nerdctl/apparmor/apparmor_unload_linux.go
similarity index 86%
rename from cmd/nerdctl/apparmor_unload_linux.go
rename to cmd/nerdctl/apparmor/apparmor_unload_linux.go
index 92fc289eb31..2ba93809544 100644
--- a/cmd/nerdctl/apparmor_unload_linux.go
+++ b/cmd/nerdctl/apparmor/apparmor_unload_linux.go
@@ -14,14 +14,16 @@
limitations under the License.
*/
-package main
+package apparmor
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/cmd/apparmor"
- "github.com/containerd/nerdctl/pkg/defaults"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/apparmor"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
)
func newApparmorUnloadCommand() *cobra.Command {
@@ -46,5 +48,5 @@ func apparmorUnloadAction(cmd *cobra.Command, args []string) error {
}
func apparmorUnloadShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteApparmorProfiles(cmd)
+ return completion.ApparmorProfiles(cmd)
}
diff --git a/cmd/nerdctl/builder.go b/cmd/nerdctl/builder/builder.go
similarity index 67%
rename from cmd/nerdctl/builder.go
rename to cmd/nerdctl/builder/builder.go
index e758fa303e2..3e8e120f23c 100644
--- a/cmd/nerdctl/builder.go
+++ b/cmd/nerdctl/builder/builder.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package builder
import (
"fmt"
@@ -22,23 +22,25 @@ import (
"os/exec"
"strings"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/sirupsen/logrus"
+ "github.com/docker/go-units"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/builder"
)
-func newBuilderCommand() *cobra.Command {
+func NewBuilderCommand() *cobra.Command {
var builderCommand = &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "builder",
Short: "Manage builds",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
builderCommand.AddCommand(
- newBuildCommand(),
+ NewBuildCommand(),
newBuilderPruneCommand(),
newBuilderDebugCommand(),
)
@@ -56,30 +58,77 @@ func newBuilderPruneCommand() *cobra.Command {
SilenceErrors: true,
}
- AddStringFlag(buildPruneCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address")
+ helpers.AddStringFlag(buildPruneCommand, "buildkit-host", nil, "", "BUILDKIT_HOST", "BuildKit address")
+
+ buildPruneCommand.Flags().BoolP("all", "a", false, "Remove all unused build cache, not just dangling ones")
+ buildPruneCommand.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
return buildPruneCommand
}
func builderPruneAction(cmd *cobra.Command, _ []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ options, err := processBuilderPruneOptions(cmd)
if err != nil {
return err
}
- buildkitHost, err := getBuildkitHost(cmd, globalOptions.Namespace)
+
+ if !options.Force {
+ var msg string
+
+ if options.All {
+ msg = "This will remove all build cache."
+ } else {
+ msg = "This will remove any dangling build cache."
+ }
+
+ if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed {
+ return err
+ }
+ }
+
+ prunedObjects, err := builder.Prune(cmd.Context(), options)
if err != nil {
return err
}
- buildctlBinary, err := buildkitutil.BuildctlBinary()
+
+ var totalReclaimedSpace int64
+
+ for _, prunedObject := range prunedObjects {
+ totalReclaimedSpace += prunedObject.Size
+ }
+
+ fmt.Fprintf(cmd.OutOrStdout(), "Total: %s\n", units.BytesSize(float64(totalReclaimedSpace)))
+
+ return nil
+}
+
+func processBuilderPruneOptions(cmd *cobra.Command) (types.BuilderPruneOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
- return err
+ return types.BuilderPruneOptions{}, err
+ }
+
+ buildkitHost, err := GetBuildkitHost(cmd, globalOptions.Namespace)
+ if err != nil {
+ return types.BuilderPruneOptions{}, err
+ }
+
+ all, err := cmd.Flags().GetBool("all")
+ if err != nil {
+ return types.BuilderPruneOptions{}, err
+ }
+
+ force, err := cmd.Flags().GetBool("force")
+ if err != nil {
+ return types.BuilderPruneOptions{}, err
}
- buildctlArgs := buildkitutil.BuildctlBaseArgs(buildkitHost)
- buildctlArgs = append(buildctlArgs, "prune")
- logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs)
- buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
- buildctlCmd.Env = os.Environ()
- buildctlCmd.Stdout = cmd.OutOrStdout()
- return buildctlCmd.Run()
+
+ return types.BuilderPruneOptions{
+ Stderr: cmd.OutOrStderr(),
+ GOptions: globalOptions,
+ BuildKitHost: buildkitHost,
+ All: all,
+ Force: force,
+ }, nil
}
func newBuilderDebugCommand() *cobra.Command {
@@ -87,7 +136,7 @@ func newBuilderDebugCommand() *cobra.Command {
var buildDebugCommand = &cobra.Command{
Use: "debug",
Short: shortHelp,
- PreRunE: checkExperimental("`nerdctl builder debug`"),
+ PreRunE: helpers.CheckExperimental("`nerdctl builder debug`"),
RunE: builderDebugAction,
SilenceUsage: true,
SilenceErrors: true,
@@ -102,7 +151,7 @@ func newBuilderDebugCommand() *cobra.Command {
}
func builderDebugAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -115,9 +164,7 @@ func builderDebugAction(cmd *cobra.Command, args []string) error {
return err
}
buildgArgs := []string{"debug"}
- if err != nil {
- return err
- } else if globalOptions.Debug {
+ if globalOptions.Debug {
buildgArgs = append([]string{"--debug"}, buildgArgs...)
}
diff --git a/cmd/nerdctl/builder_build.go b/cmd/nerdctl/builder/builder_build.go
similarity index 56%
rename from cmd/nerdctl/builder_build.go
rename to cmd/nerdctl/builder/builder_build.go
index 3b68283df9a..f22bcb8d517 100644
--- a/cmd/nerdctl/builder_build.go
+++ b/cmd/nerdctl/builder/builder_build.go
@@ -14,25 +14,27 @@
limitations under the License.
*/
-package main
+package builder
import (
"errors"
"fmt"
"os"
+ "strconv"
"strings"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/builder"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/strutil"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/buildkitutil"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/builder"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
-func newBuildCommand() *cobra.Command {
+func NewBuildCommand() *cobra.Command {
var buildCommand = &cobra.Command{
Use: "build [flags] PATH",
Short: "Build an image from a Dockerfile. Needs buildkitd to be running.",
@@ -42,7 +44,8 @@ If Dockerfile is not present and -f is not specified, it will look for Container
SilenceUsage: true,
SilenceErrors: true,
}
- AddStringFlag(buildCommand, "buildkit-host", nil, defaults.BuildKitHost(), "BUILDKIT_HOST", "BuildKit address")
+ helpers.AddStringFlag(buildCommand, "buildkit-host", nil, "", "BUILDKIT_HOST", "BuildKit address")
+ buildCommand.Flags().StringArray("add-host", nil, "Add a custom host-to-IP mapping (format: \"host:ip\")")
buildCommand.Flags().StringArrayP("tag", "t", nil, "Name and optionally a tag in the 'name:tag' format")
buildCommand.Flags().StringP("file", "f", "", "Name of the Dockerfile")
buildCommand.Flags().String("target", "", "Set the target build stage to build")
@@ -50,17 +53,29 @@ If Dockerfile is not present and -f is not specified, it will look for Container
buildCommand.Flags().Bool("no-cache", false, "Do not use cache when building the image")
buildCommand.Flags().StringP("output", "o", "", "Output destination (format: type=local,dest=path)")
buildCommand.Flags().String("progress", "auto", "Set type of progress output (auto, plain, tty). Use plain to show container output")
+ buildCommand.Flags().String("provenance", "", "Shorthand for \"--attest=type=provenance\"")
+ buildCommand.Flags().Bool("pull", false, "On true, always attempt to pull latest image version from remote. Default uses buildkit's default.")
buildCommand.Flags().StringArray("secret", nil, "Secret file to expose to the build: id=mysecret,src=/local/secret")
+ buildCommand.Flags().StringArray("allow", nil, "Allow extra privileged entitlement, e.g. network.host, security.insecure")
+ buildCommand.RegisterFlagCompletionFunc("allow", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return []string{"network.host", "security.insecure"}, cobra.ShellCompDirectiveNoFileComp
+ })
+ buildCommand.Flags().StringArray("attest", nil, "Attestation parameters (format: \"type=sbom,generator=image\")")
buildCommand.Flags().StringArray("ssh", nil, "SSH agent socket or keys to expose to the build (format: default|[=|[,]])")
buildCommand.Flags().BoolP("quiet", "q", false, "Suppress the build output and print image ID on success")
+ buildCommand.Flags().String("sbom", "", "Shorthand for \"--attest=type=sbom\"")
buildCommand.Flags().StringArray("cache-from", nil, "External cache sources (eg. user/app:cache, type=local,src=path/to/dir)")
buildCommand.Flags().StringArray("cache-to", nil, "Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir)")
buildCommand.Flags().Bool("rm", true, "Remove intermediate containers after a successful build")
-
+ buildCommand.Flags().String("network", "default", "Set type of network for build (format:network=default|none|host)")
+ buildCommand.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return []string{"default", "host", "none"}, cobra.ShellCompDirectiveNoFileComp
+ })
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
buildCommand.Flags().StringSlice("platform", []string{}, "Set target platform for build (e.g., \"amd64\", \"arm64\")")
- buildCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ buildCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
+ buildCommand.Flags().StringArray("build-context", []string{}, "Additional build contexts (e.g., name=path)")
// #endregion
buildCommand.Flags().String("iidfile", "", "Write the image ID to the file")
@@ -70,11 +85,15 @@ If Dockerfile is not present and -f is not specified, it will look for Container
}
func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBuildOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+ buildKitHost, err := GetBuildkitHost(cmd, globalOptions.Namespace)
if err != nil {
return types.BuilderBuildOptions{}, err
}
- buildKitHost, err := getBuildkitHost(cmd, globalOptions.Namespace)
+ extraHosts, err := cmd.Flags().GetStringArray("add-host")
if err != nil {
return types.BuilderBuildOptions{}, err
}
@@ -90,9 +109,6 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
if buildContext == "-" || strings.Contains(buildContext, "://") {
return types.BuilderBuildOptions{}, fmt.Errorf("unsupported build context: %q", buildContext)
}
- if err != nil {
- return types.BuilderBuildOptions{}, err
- }
output, err := cmd.Flags().GetString("output")
if err != nil {
return types.BuilderBuildOptions{}, err
@@ -125,10 +141,22 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
if err != nil {
return types.BuilderBuildOptions{}, err
}
+ var pull *bool
+ if cmd.Flags().Changed("pull") {
+ pullFlag, err := cmd.Flags().GetBool("pull")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+ pull = &pullFlag
+ }
secret, err := cmd.Flags().GetStringArray("secret")
if err != nil {
return types.BuilderBuildOptions{}, err
}
+ allow, err := cmd.Flags().GetStringArray("allow")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
ssh, err := cmd.Flags().GetStringArray("ssh")
if err != nil {
return types.BuilderBuildOptions{}, err
@@ -153,33 +181,67 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu
if err != nil {
return types.BuilderBuildOptions{}, err
}
+ network, err := cmd.Flags().GetString("network")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+
+ attest, err := cmd.Flags().GetStringArray("attest")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+ sbom, err := cmd.Flags().GetString("sbom")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+ if sbom != "" {
+ attest = append(attest, canonicalizeAttest("sbom", sbom))
+ }
+ provenance, err := cmd.Flags().GetString("provenance")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+ if provenance != "" {
+ attest = append(attest, canonicalizeAttest("provenance", provenance))
+ }
+ extendedBuildCtx, err := cmd.Flags().GetStringArray("build-context")
+ if err != nil {
+ return types.BuilderBuildOptions{}, err
+ }
+
return types.BuilderBuildOptions{
- GOptions: globalOptions,
- BuildKitHost: buildKitHost,
- BuildContext: buildContext,
- Output: output,
- Tag: tagValue,
- Progress: progress,
- File: filename,
- Target: target,
- BuildArgs: buildArgs,
- Label: label,
- NoCache: noCache,
- Secret: secret,
- SSH: ssh,
- CacheFrom: cacheFrom,
- CacheTo: cacheTo,
- Rm: rm,
- IidFile: iidfile,
- Quiet: quiet,
- Platform: platform,
- Stdout: cmd.OutOrStdout(),
- Stderr: cmd.OutOrStderr(),
- Stdin: cmd.InOrStdin(),
+ GOptions: globalOptions,
+ BuildKitHost: buildKitHost,
+ BuildContext: buildContext,
+ Output: output,
+ Tag: tagValue,
+ Progress: progress,
+ File: filename,
+ Target: target,
+ BuildArgs: buildArgs,
+ Label: label,
+ NoCache: noCache,
+ Pull: pull,
+ Secret: secret,
+ Allow: allow,
+ Attest: attest,
+ SSH: ssh,
+ CacheFrom: cacheFrom,
+ CacheTo: cacheTo,
+ Rm: rm,
+ IidFile: iidfile,
+ Quiet: quiet,
+ Platform: platform,
+ Stdout: cmd.OutOrStdout(),
+ Stderr: cmd.OutOrStderr(),
+ Stdin: cmd.InOrStdin(),
+ NetworkMode: network,
+ ExtendedBuildContext: extendedBuildCtx,
+ ExtraHosts: extraHosts,
}, nil
}
-func getBuildkitHost(cmd *cobra.Command, namespace string) (string, error) {
+func GetBuildkitHost(cmd *cobra.Command, namespace string) (string, error) {
if cmd.Flags().Changed("buildkit-host") || os.Getenv("BUILDKIT_HOST") != "" {
// If address is explicitly specified, use it.
buildkitHost, err := cmd.Flags().GetString("buildkit-host")
@@ -206,8 +268,16 @@ func buildAction(cmd *cobra.Command, args []string) error {
}
defer cancel()
- if err := builder.Build(ctx, client, options); err != nil {
- return err
+ return builder.Build(ctx, client, options)
+}
+
+// canonicalizeAttest is from https://github.com/docker/buildx/blob/v0.12/util/buildflags/attests.go##L13-L21
+func canonicalizeAttest(attestType string, in string) string {
+ if in == "" {
+ return ""
+ }
+ if b, err := strconv.ParseBool(in); err == nil {
+ return fmt.Sprintf("type=%s,disabled=%t", attestType, !b)
}
- return nil
+ return fmt.Sprintf("type=%s,%s", attestType, in)
}
diff --git a/cmd/nerdctl/builder/builder_build_oci_layout_test.go b/cmd/nerdctl/builder/builder_build_oci_layout_test.go
new file mode 100644
index 00000000000..a3fea9b5fa5
--- /dev/null
+++ b/cmd/nerdctl/builder/builder_build_oci_layout_test.go
@@ -0,0 +1,107 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package builder
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestBuildContextWithOCILayout(t *testing.T) {
+ nerdtest.Setup()
+
+ var dockerBuilderArgs []string
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(test.Windows),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if nerdtest.IsDocker() {
+ helpers.Anyhow("buildx", "stop", data.Identifier("-container"))
+ helpers.Anyhow("buildx", "rm", "--force", data.Identifier("-container"))
+ }
+ helpers.Anyhow("rmi", "-f", data.Identifier("-parent"))
+ helpers.Anyhow("rmi", "-f", data.Identifier("-child"))
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ // Default docker driver does not support OCI exporter.
+ // Reference: https://docs.docker.com/build/exporters/oci-docker/
+ if nerdtest.IsDocker() {
+ name := data.Identifier("-container")
+ helpers.Ensure("buildx", "create", "--name", name, "--driver=docker-container")
+ dockerBuilderArgs = []string{"buildx", "--builder", name}
+ }
+
+ dockerfile := fmt.Sprintf(`FROM %s
+LABEL layer=oci-layout-parent
+CMD ["echo", "test-nerdctl-build-context-oci-layout-parent"]`, testutil.CommonImage)
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+
+ tarPath := filepath.Join(buildCtx, "parent.tar")
+ dest := filepath.Join(buildCtx, "parent")
+ assert.NilError(helpers.T(), os.MkdirAll(dest, 0o700))
+ helpers.Ensure("build", buildCtx, "--tag", data.Identifier("-parent"))
+ helpers.Ensure("image", "save", "--output", tarPath, data.Identifier("-parent"))
+ helpers.Custom("tar", "Cxf", dest, tarPath).Run(&test.Expected{})
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ dockerfile := `FROM parent
+CMD ["echo", "test-nerdctl-build-context-oci-layout"]`
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+
+ var cmd test.TestableCommand
+ if nerdtest.IsDocker() {
+ cmd = helpers.Command(dockerBuilderArgs...)
+ } else {
+ cmd = helpers.Command()
+ }
+ cmd.WithArgs("build", buildCtx, fmt.Sprintf("--build-context=parent=oci-layout://%s", filepath.Join(buildCtx, "parent")), "--tag", data.Identifier("-child"))
+ if nerdtest.IsDocker() {
+ // Need to load the container image from the builder to be able to run it.
+ cmd.WithArgs("--load")
+ }
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.Contains(helpers.Capture("run", "--rm", data.Identifier("-child")), "test-nerdctl-build-context-oci-layout"), info)
+ },
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go
new file mode 100644
index 00000000000..407804f904d
--- /dev/null
+++ b/cmd/nerdctl/builder/builder_build_test.go
@@ -0,0 +1,988 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package builder
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestBuildBasics(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage)
+ err := os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", data.TempDir())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Successfully build with 'tag first', 'buildctx second'",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", "-t", data.Identifier(), data.Get("buildCtx"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ {
+ Description: "Successfully build with 'buildctx first', 'tag second'",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ {
+ Description: "Successfully build with output docker, main tag still works",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ {
+ Description: "Successfully build with output docker, name cannot be used",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier(), "--output=type=docker,name="+data.Identifier("ignored"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier("ignored"))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(-1, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestCanBuildOnOtherPlatform(t *testing.T) {
+ nerdtest.Setup()
+
+ requireEmulation := &test.Requirement{
+ Check: func(data test.Data, helpers test.Helpers) (bool, string) {
+ candidateArch := "arm64"
+ if runtime.GOARCH == "arm64" {
+ candidateArch = "amd64"
+ }
+ can, err := platformutil.CanExecProbably("linux/" + candidateArch)
+ assert.NilError(helpers.T(), err)
+
+ data.Set("OS", "linux")
+ data.Set("Architecture", candidateArch)
+ return can, "Current environment does not support emulation"
+ },
+ }
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ requireEmulation,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN echo hello > /hello
+CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage)
+ err := os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", data.TempDir())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "--platform", fmt.Sprintf("%s/%s", data.Get("OS"), data.Get("Architecture")), "-t", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
+
+// TestBuildBaseImage tests if an image can be built on the previously built image.
+// This isn't currently supported by nerdctl with BuildKit OCI worker.
+func TestBuildBaseImage(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("first"))
+ helpers.Anyhow("rmi", "-f", data.Identifier("second"))
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN echo hello > /hello
+CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage)
+ err := os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", data.Identifier("first"), data.TempDir())
+
+ dockerfileSecond := fmt.Sprintf(`FROM %s
+RUN echo hello2 > /hello2
+CMD ["cat", "/hello2"]`, data.Identifier("first"))
+ err = os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfileSecond), 0644)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", data.Identifier("second"), data.TempDir())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier("second"))
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello2\n")),
+ }
+
+ testCase.Run(t)
+}
+
+// TestBuildFromContainerd tests if an image can be built on an image pulled by nerdctl.
+// This isn't currently supported by nerdctl with BuildKit OCI worker.
+func TestBuildFromContainerd(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("first"))
+ helpers.Anyhow("rmi", "-f", data.Identifier("second"))
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ helpers.Ensure("tag", testutil.CommonImage, data.Identifier("first"))
+
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN echo hello2 > /hello2
+CMD ["cat", "/hello2"]`, data.Identifier("first"))
+ err := os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", data.Identifier("second"), data.TempDir())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier("second"))
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello2\n")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildFromStdin(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-stdin"]`, testutil.CommonImage)
+ cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "-", ".")
+ cmd.WithStdin(strings.NewReader(dockerfile))
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Errors: []error{errors.New(data.Identifier())},
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithDockerfile(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-dockerfile"]
+ `, testutil.CommonImage)
+ buildCtx := filepath.Join(data.TempDir(), "test")
+ err := os.MkdirAll(buildCtx, 0755)
+ assert.NilError(helpers.T(), err)
+ err = os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Dockerfile ..",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "Dockerfile", "..")
+ cmd.WithCwd(data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "Dockerfile .",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "Dockerfile", ".")
+ cmd.WithCwd(data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "../Dockerfile .",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("build", "-t", data.Identifier(), "-f", "../Dockerfile", ".")
+ cmd.WithCwd(data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: test.Expects(1, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildLocal(t *testing.T) {
+ nerdtest.Setup()
+
+ const testFileName = "nerdctl-build-test"
+ const testContent = "nerdctl"
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM scratch
+COPY %s /`, testFileName)
+
+ err := os.WriteFile(filepath.Join(data.TempDir(), "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+
+ err = os.WriteFile(filepath.Join(data.TempDir(), testFileName), []byte(testContent), 0644)
+ assert.NilError(helpers.T(), err)
+
+ data.Set("buildCtx", data.TempDir())
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "destination 1",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", "-o", fmt.Sprintf("type=local,dest=%s", data.TempDir()), data.Get("buildCtx"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ testFilePath := filepath.Join(data.TempDir(), testFileName)
+ _, err := os.Stat(testFilePath)
+ assert.NilError(helpers.T(), err, info)
+ dt, err := os.ReadFile(testFilePath)
+ assert.NilError(helpers.T(), err, info)
+ assert.Equal(helpers.T(), string(dt), testContent, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "destination 2",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", "-o", data.TempDir(), data.Get("buildCtx"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ testFilePath := filepath.Join(data.TempDir(), testFileName)
+ _, err := os.Stat(testFilePath)
+ assert.NilError(helpers.T(), err, info)
+ dt, err := os.ReadFile(testFilePath)
+ assert.NilError(helpers.T(), err, info)
+ assert.Equal(helpers.T(), string(dt), testContent, info)
+ },
+ }
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithBuildArg(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+ARG TEST_STRING=1
+ENV TEST_STRING=$TEST_STRING
+CMD echo $TEST_STRING
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "No args",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("1\n")),
+ },
+ {
+ Description: "ArgValueOverridesDefault",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING=2", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("2\n")),
+ },
+ {
+ Description: "EmptyArgValueOverridesDefault",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING=", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("\n")),
+ },
+ {
+ Description: "UnsetArgKeyPreservesDefault",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("1\n")),
+ },
+ {
+ Description: "EnvValueOverridesDefault",
+ Env: map[string]string{
+ "TEST_STRING": "3",
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("3\n")),
+ },
+ {
+ Description: "EmptyEnvValueOverridesDefault",
+ Env: map[string]string{
+ "TEST_STRING": "",
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("\n")),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithIIDFile(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx, "--iidfile", filepath.Join(data.TempDir(), "id.txt"), "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ imageID, err := os.ReadFile(filepath.Join(data.TempDir(), "id.txt"))
+ assert.NilError(helpers.T(), err)
+ return helpers.Command("run", "--rm", string(imageID))
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithLabels(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+LABEL name=nerdctl-build-test-label
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx, "--label", "label=test", "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("inspect", data.Identifier(), "--format", "{{json .Config.Labels }}")
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildMultipleTags(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Data: test.WithData("i1", "image").
+ Set("i2", "image2").
+ Set("i3", "image3:hello"),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Get("i1"))
+ helpers.Anyhow("rmi", "-f", data.Get("i2"))
+ helpers.Anyhow("rmi", "-f", data.Get("i3"))
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx, "-t", data.Get("i1"), "-t", data.Get("i2"), "-t", data.Get("i3"))
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "i1",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get("i1"))
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ {
+ Description: "i2",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get("i2"))
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ {
+ Description: "i3",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get("i3"))
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithContainerfile(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Containerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx, "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildWithDockerFileAndContainerfile(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "dockerfile"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ dockerfile = fmt.Sprintf(`FROM %s
+CMD ["echo", "containerfile"]
+ `, testutil.CommonImage)
+ err = os.WriteFile(filepath.Join(buildCtx, "Containerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx, "-t", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("dockerfile\n")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildNoTag(t *testing.T) {
+ nerdtest.Setup()
+
+ // FIXME: this test should be rewritten and instead get the image id from the build, then query the image explicitly - instead of pruning / noparallel
+ testCase := &test.Case{
+ NoParallel: true,
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("image", "prune", "--force", "--all")
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx)
+ },
+ Command: test.Command("images"),
+ Expected: test.Expects(0, nil, test.Contains("")),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildContextDockerImageAlias(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := `FROM myorg/myapp
+CMD ["echo", "nerdctl-build-myorg/myapp"]`
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", "-t", data.Identifier(), data.Get("buildCtx"), fmt.Sprintf("--build-context=myorg/myapp=docker-image://%s", testutil.CommonImage))
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildContextWithCopyFromDir(t *testing.T) {
+ nerdtest.Setup()
+
+ content := "hello_from_dir_2"
+ filename := "hello.txt"
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dir2 := helpers.T().TempDir()
+ filePath := filepath.Join(dir2, filename)
+ err := os.WriteFile(filePath, []byte(content), 0o600)
+ assert.NilError(helpers.T(), err)
+ dockerfile := fmt.Sprintf(`FROM %s
+COPY --from=dir2 /%s /hello_from_dir2.txt
+RUN ["cat", "/hello_from_dir2.txt"]`, testutil.CommonImage, filename)
+ buildCtx := data.TempDir()
+ err = os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ data.Set("dir2", dir2)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", "-t", data.Identifier(), data.Get("buildCtx"), fmt.Sprintf("--build-context=dir2=%s", data.Get("dir2")))
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
+
+// TestBuildSourceDateEpoch tests that $SOURCE_DATE_EPOCH is propagated from the client env
+// https://github.com/docker/buildx/pull/1482
+func TestBuildSourceDateEpoch(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+ARG SOURCE_DATE_EPOCH
+RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch
+CMD ["cat", "/source-date-epoch"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "1111111111",
+ Env: map[string]string{
+ "SOURCE_DATE_EPOCH": "1111111111",
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("1111111111\n")),
+ },
+ {
+ Description: "2222222222",
+ Env: map[string]string{
+ "SOURCE_DATE_EPOCH": "1111111111",
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "SOURCE_DATE_EPOCH=2222222222", "-t", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, test.Equals("2222222222\n")),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildNetwork(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN apk add --no-cache curl
+RUN curl -I http://google.com
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "none",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "none")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(1, nil, nil),
+ },
+ {
+ Description: "empty",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "default",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "default")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildAttestation(t *testing.T) {
+ nerdtest.Setup()
+
+ const testSBOMFileName = "sbom.spdx.json"
+ const testProvenanceFileName = "provenance.json"
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(nerdtest.Docker),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if nerdtest.IsDocker() {
+ helpers.Anyhow("buildx", "rm", data.Identifier("builder"))
+ }
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ if nerdtest.IsDocker() {
+ helpers.Anyhow("buildx", "create", "--name", data.Identifier("builder"), "--bootstrap", "--use")
+ }
+
+ dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "SBOM",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ outputSBOMDir := helpers.T().TempDir()
+ data.Set("outputSBOMFile", filepath.Join(outputSBOMDir, testSBOMFileName))
+
+ cmd := helpers.Command("build")
+ if nerdtest.IsDocker() {
+ cmd.WithArgs("--builder", data.Identifier("builder"))
+ }
+ cmd.WithArgs("--sbom=true", "-o", fmt.Sprintf("type=local,dest=%s", outputSBOMDir), data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := os.Stat(data.Get("outputSBOMFile"))
+ assert.NilError(t, err, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Provenance",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ outputProvenanceDir := data.TempDir()
+ data.Set("outputProvenanceFile", filepath.Join(outputProvenanceDir, testProvenanceFileName))
+
+ cmd := helpers.Command("build")
+ if nerdtest.IsDocker() {
+ cmd.WithArgs("--builder", data.Identifier("builder"))
+ }
+ cmd.WithArgs("--provenance=mode=min", "-o", fmt.Sprintf("type=local,dest=%s", outputProvenanceDir), data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := os.Stat(data.Get("outputProvenanceFile"))
+ assert.NilError(t, err, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Attestation",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ outputAttestationDir := data.TempDir()
+ data.Set("outputSBOMFile", filepath.Join(outputAttestationDir, testSBOMFileName))
+ data.Set("outputProvenanceFile", filepath.Join(outputAttestationDir, testProvenanceFileName))
+
+ cmd := helpers.Command("build")
+ if nerdtest.IsDocker() {
+ cmd.WithArgs("--builder", data.Identifier("builder"))
+ }
+ cmd.WithArgs("--attest=type=provenance,mode=min", "--attest=type=sbom", "-o", fmt.Sprintf("type=local,dest=%s", outputAttestationDir), data.Get("buildCtx"))
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := os.Stat(data.Get("outputSBOMFile"))
+ assert.NilError(t, err, info)
+ _, err = os.Stat(data.Get("outputProvenanceFile"))
+ assert.NilError(t, err, info)
+ },
+ }
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestBuildAddHost(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.Build,
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN ping -c 5 alpha
+RUN ping -c 5 beta
+`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "-t", data.Identifier(), "--add-host", "alpha:127.0.0.1", "--add-host", "beta:127.0.0.1")
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/builder/builder_builder_test.go b/cmd/nerdctl/builder/builder_builder_test.go
new file mode 100644
index 00000000000..f1092680a40
--- /dev/null
+++ b/cmd/nerdctl/builder/builder_builder_test.go
@@ -0,0 +1,158 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package builder
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestBuilder(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ NoParallel: true,
+ Require: test.Require(
+ nerdtest.Build,
+ test.Not(test.Windows),
+ ),
+ SubTests: []*test.Case{
+ {
+ Description: "PruneForce",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx)
+ },
+ Command: test.Command("builder", "prune", "--force"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "PruneForceAll",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx)
+ },
+ Command: test.Command("builder", "prune", "--force", "--all"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "Debug",
+ // `nerdctl builder debug` is currently incompatible with `docker buildx debug`.
+ Require: test.Require(test.Not(nerdtest.Docker)),
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ cmd := helpers.Command("builder", "debug", buildCtx)
+ cmd.WithStdin(bytes.NewReader([]byte("c\n")))
+ return cmd
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "WithPull",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ // FIXME: this test should be rewritten to dynamically retrieve the ids, and use images
+ // available on all platforms
+ oldImage := testutil.BusyboxImage
+ oldImageSha := "7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c"
+ newImage := testutil.AlpineImage
+ newImageSha := "ec14c7992a97fc11425907e908340c6c3d6ff602f5f13d899e6b7027c9b4133a"
+
+ helpers.Ensure("pull", "--quiet", oldImage)
+ helpers.Ensure("tag", oldImage, newImage)
+
+ dockerfile := fmt.Sprintf(`FROM %s`, newImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+
+ data.Set("buildCtx", buildCtx)
+ data.Set("oldImageSha", oldImageSha)
+ data.Set("newImageSha", newImageSha)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", testutil.AlpineImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "pull false",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "--pull=false")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Errors: []error{errors.New(data.Get("oldImageSha"))},
+ }
+ },
+ },
+ {
+ Description: "pull true",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"), "--pull=true")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Errors: []error{errors.New(data.Get("newImageSha"))},
+ }
+ },
+ },
+ {
+ Description: "no pull",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Errors: []error{errors.New(data.Get("newImageSha"))},
+ }
+ },
+ },
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/builder/builder_test.go b/cmd/nerdctl/builder/builder_test.go
new file mode 100644
index 00000000000..fee2491cb9b
--- /dev/null
+++ b/cmd/nerdctl/builder/builder_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package builder
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/builder_build_test.go b/cmd/nerdctl/builder_build_test.go
deleted file mode 100644
index b2133ecd6e7..00000000000
--- a/cmd/nerdctl/builder_build_test.go
+++ /dev/null
@@ -1,451 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestBuild(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("build", buildCtx, "-t", imageName).AssertOK()
- ignoredImageNamed := imageName + "-" + "ignored"
- outputOpt := fmt.Sprintf("--output=type=docker,name=%s", ignoredImageNamed)
- base.Cmd("build", buildCtx, "-t", imageName, outputOpt).AssertOK()
-
- base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n")
- base.Cmd("run", "--rm", ignoredImageNamed).AssertFail()
-}
-
-// TestBuildBaseImage tests if an image can be built on the previously built image.
-// This isn't currently supported by nerdctl with BuildKit OCI worker.
-func TestBuildBaseImage(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
- imageName2 := imageName + "-2"
- defer base.Cmd("rmi", imageName2).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-RUN echo hello > /hello
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("build", buildCtx, "-t", imageName).AssertOK()
-
- dockerfile2 := fmt.Sprintf(`FROM %s
-RUN echo hello2 > /hello2
-CMD ["cat", "/hello2"]
- `, imageName)
-
- buildCtx2, err := createBuildContext(dockerfile2)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx2)
-
- base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK()
- base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK()
-
- base.Cmd("run", "--rm", imageName2).AssertOutExactly("hello2\n")
-}
-
-// TestBuildFromContainerd tests if an image can be built on an image pulled by nerdctl.
-// This isn't currently supported by nerdctl with BuildKit OCI worker.
-func TestBuildFromContainerd(t *testing.T) {
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
- imageName2 := imageName + "-2"
- defer base.Cmd("rmi", imageName2).Run()
-
- // FIXME: BuildKit sometimes tries to use base image manifests of platforms that hasn't been
- // pulled by `nerdctl pull`. This leads to "not found" error for the base image.
- // To avoid this issue, images shared to BuildKit should always be pulled by manifest
- // digest or `--all-platforms` needs to be added.
- base.Cmd("pull", "--all-platforms", testutil.CommonImage).AssertOK()
- base.Cmd("tag", testutil.CommonImage, imageName).AssertOK()
- base.Cmd("rmi", testutil.CommonImage).AssertOK()
-
- dockerfile2 := fmt.Sprintf(`FROM %s
-RUN echo hello2 > /hello2
-CMD ["cat", "/hello2"]
- `, imageName)
-
- buildCtx2, err := createBuildContext(dockerfile2)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx2)
-
- base.Cmd("build", "-t", imageName2, buildCtx2).AssertOK()
- base.Cmd("build", buildCtx2, "-t", imageName2).AssertOK()
-
- base.Cmd("run", "--rm", imageName2).AssertOutExactly("hello2\n")
-}
-
-func TestBuildFromStdin(t *testing.T) {
- t.Parallel()
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-stdin"]
- `, testutil.CommonImage)
-
- base.Cmd("build", "-t", imageName, "-f", "-", ".").CmdOption(testutil.WithStdin(strings.NewReader(dockerfile))).AssertCombinedOutContains(imageName)
-}
-
-func TestBuildWithDockerfile(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-dockerfile"]
- `, testutil.CommonImage)
-
- buildCtx := filepath.Join(t.TempDir(), "test")
- err := os.MkdirAll(buildCtx, 0755)
- assert.NilError(t, err)
- err = os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0644)
- assert.NilError(t, err)
-
- pwd, err := os.Getwd()
- assert.NilError(t, err)
- err = os.Chdir(buildCtx)
- assert.NilError(t, err)
- defer os.Chdir(pwd)
-
- // hack os.Getwd return "(unreachable)" on rootless
- t.Setenv("PWD", buildCtx)
-
- base.Cmd("build", "-t", imageName, "-f", "Dockerfile", "..").AssertOK()
- base.Cmd("build", "-t", imageName, "-f", "Dockerfile", ".").AssertOK()
- // fail err: no such file or directory
- base.Cmd("build", "-t", imageName, "-f", "../Dockerfile", ".").AssertFail()
-}
-
-func TestBuildLocal(t *testing.T) {
- t.Parallel()
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- if testutil.GetTarget() == testutil.Docker {
- base.Env = append(base.Env, "DOCKER_BUILDKIT=1")
- }
- defer base.Cmd("builder", "prune").Run()
- const testFileName = "nerdctl-build-test"
- const testContent = "nerdctl"
- outputDir := t.TempDir()
-
- dockerfile := fmt.Sprintf(`FROM scratch
-COPY %s /`,
- testFileName)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- if err := os.WriteFile(filepath.Join(buildCtx, testFileName), []byte(testContent), 0644); err != nil {
- t.Fatal(err)
- }
-
- testFilePath := filepath.Join(outputDir, testFileName)
- base.Cmd("build", "-o", fmt.Sprintf("type=local,dest=%s", outputDir), buildCtx).AssertOK()
- if _, err := os.Stat(testFilePath); err != nil {
- t.Fatal(err)
- }
- data, err := os.ReadFile(testFilePath)
- assert.NilError(t, err)
- assert.Equal(t, string(data), testContent)
-
- aliasOutputDir := t.TempDir()
- testAliasFilePath := filepath.Join(aliasOutputDir, testFileName)
- base.Cmd("build", "-o", aliasOutputDir, buildCtx).AssertOK()
- if _, err := os.Stat(testAliasFilePath); err != nil {
- t.Fatal(err)
- }
- data, err = os.ReadFile(testAliasFilePath)
- assert.NilError(t, err)
- assert.Equal(t, string(data), testContent)
-}
-
-func createBuildContext(dockerfile string) (string, error) {
- tmpDir, err := os.MkdirTemp("", "nerdctl-build-test")
- if err != nil {
- return "", err
- }
- if err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644); err != nil {
- return "", err
- }
- return tmpDir, nil
-}
-
-func TestBuildWithBuildArg(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-ARG TEST_STRING=1
-ENV TEST_STRING=$TEST_STRING
-CMD echo $TEST_STRING
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", buildCtx, "-t", imageName).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly("1\n")
-
- validCases := []struct {
- name string
- arg string
- envValue string
- envSet bool
- expected string
- }{
- {"ArgValueOverridesDefault", "TEST_STRING=2", "", false, "2\n"},
- {"EmptyArgValueOverridesDefault", "TEST_STRING=", "", false, "\n"},
- {"UnsetArgKeyPreservesDefault", "TEST_STRING", "", false, "1\n"},
- {"EnvValueOverridesDefault", "TEST_STRING", "3", true, "3\n"},
- {"EmptyEnvValueOverridesDefault", "TEST_STRING", "", true, "\n"},
- }
-
- for _, tc := range validCases {
- t.Run(tc.name, func(t *testing.T) {
- if tc.envSet {
- err := os.Setenv("TEST_STRING", tc.envValue)
- assert.NilError(t, err)
- defer os.Unsetenv("TEST_STRING")
- }
-
- base.Cmd("build", buildCtx, "-t", imageName, "--build-arg", tc.arg).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly(tc.expected)
- })
- }
-
- t.Run("InvalidBuildArgCausesError", func(t *testing.T) {
- base.Cmd("build", buildCtx, "-t", imageName, "--build-arg", "=TEST_STRING").AssertFail()
- })
-}
-
-func TestBuildWithIIDFile(t *testing.T) {
- t.Parallel()
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
- fileName := filepath.Join(t.TempDir(), "id.txt")
-
- base.Cmd("build", "-t", imageName, buildCtx, "--iidfile", fileName).AssertOK()
- base.Cmd("build", buildCtx, "-t", imageName, "--iidfile", fileName).AssertOK()
- defer os.Remove(fileName)
-
- imageID, err := os.ReadFile(fileName)
- assert.NilError(t, err)
-
- base.Cmd("run", "--rm", string(imageID)).AssertOutExactly("nerdctl-build-test-string\n")
-}
-
-func TestBuildWithLabels(t *testing.T) {
- t.Parallel()
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
-
- dockerfile := fmt.Sprintf(`FROM %s
-LABEL name=nerdctl-build-test-label
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", imageName, buildCtx, "--label", "label=test").AssertOK()
- defer base.Cmd("rmi", imageName).Run()
-
- base.Cmd("inspect", imageName, "--format", "{{json .Config.Labels }}").AssertOutExactly("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n")
-}
-
-func TestBuildMultipleTags(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- img := testutil.Identifier(t)
- imgWithNoTag, imgWithCustomTag := fmt.Sprintf("%s%d", img, 2), fmt.Sprintf("%s%d:hello", img, 3)
- defer base.Cmd("rmi", img).Run()
- defer base.Cmd("rmi", imgWithNoTag).Run()
- defer base.Cmd("rmi", imgWithCustomTag).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", img, buildCtx).AssertOK()
- base.Cmd("build", buildCtx, "-t", img, "-t", imgWithNoTag, "-t", imgWithCustomTag).AssertOK()
- base.Cmd("run", "--rm", img).AssertOutExactly("nerdctl-build-test-string\n")
- base.Cmd("run", "--rm", imgWithNoTag).AssertOutExactly("nerdctl-build-test-string\n")
- base.Cmd("run", "--rm", imgWithCustomTag).AssertOutExactly("nerdctl-build-test-string\n")
-}
-
-func TestBuildWithContainerfile(t *testing.T) {
- testutil.RequiresBuild(t)
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- containerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx := t.TempDir()
-
- var err = os.WriteFile(filepath.Join(buildCtx, "Containerfile"), []byte(containerfile), 0644)
- assert.NilError(t, err)
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly("nerdctl-build-test-string\n")
-}
-
-func TestBuildWithDockerFileAndContainerfile(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "dockerfile"]
- `, testutil.CommonImage)
-
- containerfile := fmt.Sprintf(`FROM %s
- CMD ["echo", "containerfile"]
- `, testutil.CommonImage)
-
- tmpDir := t.TempDir()
-
- var err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644)
- assert.NilError(t, err)
-
- err = os.WriteFile(filepath.Join(tmpDir, "Containerfile"), []byte(containerfile), 0644)
- assert.NilError(t, err)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly("dockerfile\n")
-}
-
-func TestBuildNoTag(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").AssertOK()
- base.Cmd("image", "prune", "--force", "--all").AssertOK()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-notag-string"]
- `, testutil.CommonImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", buildCtx).AssertOK()
- base.Cmd("images").AssertOutContains("")
- base.Cmd("image", "prune", "--force", "--all").AssertOK()
-}
-
-// TestBuildSourceDateEpoch tests that $SOURCE_DATE_EPOCH is propagated from the client env
-// https://github.com/docker/buildx/pull/1482
-func TestBuildSourceDateEpoch(t *testing.T) {
- testutil.RequiresBuild(t)
- testutil.DockerIncompatible(t) // Needs buildx v0.10 (https://github.com/docker/buildx/pull/1489)
- base := testutil.NewBase(t)
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).AssertOK()
-
- dockerfile := fmt.Sprintf(`FROM %s
-ARG SOURCE_DATE_EPOCH
-RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch
-CMD ["cat", "/source-date-epoch"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- const sourceDateEpochEnvStr = "1111111111"
- t.Setenv("SOURCE_DATE_EPOCH", sourceDateEpochEnvStr)
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly(sourceDateEpochEnvStr + "\n")
-
- const sourceDateEpochArgStr = "2222222222"
- base.Cmd("build", "-t", imageName, "--build-arg", "SOURCE_DATE_EPOCH="+sourceDateEpochArgStr, buildCtx).AssertOK()
- base.Cmd("run", "--rm", imageName).AssertOutExactly(sourceDateEpochArgStr + "\n")
-}
diff --git a/cmd/nerdctl/completion.go b/cmd/nerdctl/completion/completion.go
similarity index 66%
rename from cmd/nerdctl/completion.go
rename to cmd/nerdctl/completion/completion.go
index aefc30b45da..7718c1bb063 100644
--- a/cmd/nerdctl/completion.go
+++ b/cmd/nerdctl/completion/completion.go
@@ -14,21 +14,27 @@
limitations under the License.
*/
-package main
+package completion
import (
"context"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/netutil"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
-func shellCompleteImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+func ImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -50,8 +56,8 @@ func shellCompleteImageNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirec
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-func shellCompleteContainerNames(cmd *cobra.Command, filterFunc func(containerd.ProcessStatus) bool) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+func ContainerNames(cmd *cobra.Command, filterFunc func(containerd.ProcessStatus) bool) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -98,9 +104,9 @@ func shellCompleteContainerNames(cmd *cobra.Command, filterFunc func(containerd.
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-// shellCompleteNetworkNames includes {"bridge","host","none"}
-func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+// NetworkNames includes {"bridge","host","none"}
+func NetworkNames(cmd *cobra.Command, exclude []string) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -109,7 +115,7 @@ func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string,
excludeMap[ex] = struct{}{}
}
- e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath)
+ e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace))
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -118,9 +124,13 @@ func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string,
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
- for netName := range netConfigs {
+ for netName, network := range netConfigs {
if _, ok := excludeMap[netName]; !ok {
candidates = append(candidates, netName)
+ if network.NerdctlID != nil {
+ candidates = append(candidates, *network.NerdctlID)
+ candidates = append(candidates, (*network.NerdctlID)[0:12])
+ }
}
}
for _, s := range []string{"host", "none"} {
@@ -131,8 +141,8 @@ func shellCompleteNetworkNames(cmd *cobra.Command, exclude []string) ([]string,
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-func shellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+func VolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -147,7 +157,7 @@ func shellCompleteVolumeNames(cmd *cobra.Command) ([]string, cobra.ShellCompDire
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-func shellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+func Platforms(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
candidates := []string{
"amd64",
"arm64",
@@ -160,3 +170,12 @@ func shellCompletePlatforms(cmd *cobra.Command, args []string, toComplete string
}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
+
+func getVolumes(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (map[string]native.Volume, error) {
+ volumeSize, err := cmd.Flags().GetBool("size")
+ if err != nil {
+ // The `nerdctl volume rm` does not have the flag `size`, so set it to false as the default value.
+ volumeSize = false
+ }
+ return volume.Volumes(globalOptions.Namespace, globalOptions.DataRoot, globalOptions.Address, volumeSize, nil)
+}
diff --git a/cmd/nerdctl/completion/completion_freebsd.go b/cmd/nerdctl/completion/completion_freebsd.go
new file mode 100644
index 00000000000..465671cfc24
--- /dev/null
+++ b/cmd/nerdctl/completion/completion_freebsd.go
@@ -0,0 +1,23 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package completion
+
+import "github.com/spf13/cobra"
+
+func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/nerdctl/completion/completion_linux.go b/cmd/nerdctl/completion/completion_linux.go
new file mode 100644
index 00000000000..38a992c19b5
--- /dev/null
+++ b/cmd/nerdctl/completion/completion_linux.go
@@ -0,0 +1,48 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package completion
+
+import (
+ "github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
+ ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+)
+
+func ApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
+ profiles, err := apparmorutil.Profiles()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+ var names []string // nolint: prealloc
+ for _, f := range profiles {
+ names = append(names, f.Name)
+ }
+ return names, cobra.ShellCompDirectiveNoFileComp
+}
+
+func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ candidates := []string{"cgroupfs"}
+ if ncdefaults.IsSystemdAvailable() {
+ candidates = append(candidates, "systemd")
+ }
+ if rootlessutil.IsRootless() {
+ candidates = append(candidates, "none")
+ }
+ return candidates, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/nerdctl/completion/completion_test.go b/cmd/nerdctl/completion/completion_test.go
new file mode 100644
index 00000000000..84ba1377153
--- /dev/null
+++ b/cmd/nerdctl/completion/completion_test.go
@@ -0,0 +1,216 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package completion
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
+
+func TestCompletion(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ helpers.Ensure("network", "create", identifier)
+ helpers.Ensure("volume", "create", identifier)
+ data.Set("identifier", identifier)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("network", "rm", identifier)
+ helpers.Anyhow("volume", "rm", identifier)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "--cgroup-manager",
+ Require: test.Not(test.Windows),
+ Command: test.Command("__complete", "--cgroup-manager", ""),
+ Expected: test.Expects(0, nil, test.Contains("cgroupfs\n")),
+ },
+ {
+ Description: "--snapshotter",
+ Require: test.Not(test.Windows),
+ Command: test.Command("__complete", "--snapshotter", ""),
+ Expected: test.Expects(0, nil, test.Contains("native\n")),
+ },
+ {
+ Description: "empty",
+ Command: test.Command("__complete", ""),
+ Expected: test.Expects(0, nil, test.Contains("run\t")),
+ },
+ {
+ Description: "build --network",
+ Command: test.Command("__complete", "build", "--network", ""),
+ Expected: test.Expects(0, nil, test.Contains("default\n")),
+ },
+ {
+ Description: "run -",
+ Command: test.Command("__complete", "run", "-"),
+ Expected: test.Expects(0, nil, test.Contains("--network\t")),
+ },
+ {
+ Description: "run --n",
+ Command: test.Command("__complete", "run", "--n"),
+ Expected: test.Expects(0, nil, test.Contains("--network\t")),
+ },
+ {
+ Description: "run --ne",
+ Command: test.Command("__complete", "run", "--ne"),
+ Expected: test.Expects(0, nil, test.Contains("--network\t")),
+ },
+ {
+ Description: "run --net",
+ Command: test.Command("__complete", "run", "--net", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains("host\n"),
+ test.Contains(data.Get("identifier")+"\n"),
+ ),
+ }
+ },
+ },
+ {
+ Description: "run -it --net",
+ Command: test.Command("__complete", "run", "-it", "--net", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains("host\n"),
+ test.Contains(data.Get("identifier")+"\n"),
+ ),
+ }
+ },
+ },
+ {
+ Description: "run -ti --rm --net",
+ Command: test.Command("__complete", "run", "-it", "--rm", "--net", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains("host\n"),
+ test.Contains(data.Get("identifier")+"\n"),
+ ),
+ }
+ },
+ },
+ {
+ Description: "run --restart",
+ Command: test.Command("__complete", "run", "--restart", ""),
+ Expected: test.Expects(0, nil, test.Contains("always\n")),
+ },
+ {
+ Description: "network --rm",
+ Command: test.Command("__complete", "network", "rm", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain("host\n"),
+ test.Contains(data.Get("identifier")+"\n"),
+ ),
+ }
+ },
+ },
+ {
+ Description: "run --cap-add",
+ Require: test.Not(test.Windows),
+ Command: test.Command("__complete", "run", "--cap-add", ""),
+ Expected: test.Expects(0, nil, test.All(
+ test.Contains("sys_admin\n"),
+ test.DoesNotContain("CAP_SYS_ADMIN\n"),
+ )),
+ },
+ {
+ Description: "volume inspect",
+ Command: test.Command("__complete", "volume", "inspect", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("identifier") + "\n"),
+ }
+ },
+ },
+ {
+ Description: "volume rm",
+ Command: test.Command("__complete", "volume", "rm", ""),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("identifier") + "\n"),
+ }
+ },
+ },
+ {
+ Description: "no namespace --cgroup-manager",
+ Require: test.Not(test.Windows),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("nerdctl", "__complete", "--cgroup-manager", "")
+ },
+ Expected: test.Expects(0, nil, test.Contains("cgroupfs\n")),
+ },
+ {
+ Description: "no namespace empty",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("nerdctl", "__complete", "")
+ },
+ Expected: test.Expects(0, nil, test.Contains("run\t")),
+ },
+ {
+ Description: "namespace space empty",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"}
+ return helpers.Custom("nerdctl", "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "")
+ },
+ Expected: test.Expects(0, nil, test.Contains("run\t")),
+ },
+ {
+ Description: "run -i",
+ Command: test.Command("__complete", "run", "-i", ""),
+ Expected: test.Expects(0, nil, test.Contains(testutil.CommonImage)),
+ },
+ {
+ Description: "run -it",
+ Command: test.Command("__complete", "run", "-it", ""),
+ Expected: test.Expects(0, nil, test.Contains(testutil.CommonImage)),
+ },
+ {
+ Description: "run -it --rm",
+ Command: test.Command("__complete", "run", "-it", "--rm", ""),
+ Expected: test.Expects(0, nil, test.Contains(testutil.CommonImage)),
+ },
+ {
+ Description: "namespace run -i",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"}
+ return helpers.Custom("nerdctl", "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "run", "-i", "")
+ },
+ Expected: test.Expects(0, nil, test.Contains(testutil.CommonImage+"\n")),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/main_unix.go b/cmd/nerdctl/completion/completion_unix.go
similarity index 65%
rename from cmd/nerdctl/main_unix.go
rename to cmd/nerdctl/completion/completion_unix.go
index ce8e8f9656a..af0b8698ce2 100644
--- a/cmd/nerdctl/main_unix.go
+++ b/cmd/nerdctl/completion/completion_unix.go
@@ -1,4 +1,4 @@
-//go:build freebsd || linux
+//go:build unix
/*
Copyright The containerd Authors.
@@ -16,18 +16,30 @@
limitations under the License.
*/
-package main
+package completion
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
-func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+func NetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ candidates := []string{"bridge", "macvlan", "ipvlan"}
+ return candidates, cobra.ShellCompDirectiveNoFileComp
+}
+
+func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return []string{"default", "host-local", "dhcp"}, cobra.ShellCompDirectiveNoFileComp
+}
+
+func NamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
@@ -35,9 +47,7 @@ func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete s
_ = rootlessutil.ParentMain(globalOptions.HostGatewayIP)
return nil, cobra.ShellCompDirectiveNoFileComp
}
- if err != nil {
- return nil, cobra.ShellCompDirectiveNoFileComp
- }
+
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
if err != nil {
return nil, cobra.ShellCompDirectiveError
@@ -46,7 +56,7 @@ func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete s
nsService := client.NamespaceService()
nsList, err := nsService.List(ctx)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
return nil, cobra.ShellCompDirectiveError
}
var candidates []string
@@ -54,8 +64,8 @@ func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete s
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-func shellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- globalOptions, err := processRootCmdFlags(cmd)
+func SnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return nil, cobra.ShellCompDirectiveError
}
diff --git a/cmd/nerdctl/completion/completion_windows.go b/cmd/nerdctl/completion/completion_windows.go
new file mode 100644
index 00000000000..020e0594926
--- /dev/null
+++ b/cmd/nerdctl/completion/completion_windows.go
@@ -0,0 +1,40 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package completion
+
+import "github.com/spf13/cobra"
+
+func NamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+func SnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+func CgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveNoFileComp
+}
+
+func NetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ candidates := []string{"nat"}
+ return candidates, cobra.ShellCompDirectiveNoFileComp
+}
+
+func IPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return []string{"default"}, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/nerdctl/completion_linux_test.go b/cmd/nerdctl/completion_linux_test.go
deleted file mode 100644
index ccff901371e..00000000000
--- a/cmd/nerdctl/completion_linux_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestCompletion(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- const gsc = "__complete"
- // cmd is executed with base.Args={"--namespace=nerdctl-test"}
- base.Cmd(gsc, "--cgroup-manager", "").AssertOutContains("cgroupfs\n")
- base.Cmd(gsc, "--snapshotter", "").AssertOutContains("native\n")
- base.Cmd(gsc, "").AssertOutContains("run\t")
- base.Cmd(gsc, "run", "-").AssertOutContains("--network\t")
- base.Cmd(gsc, "run", "--n").AssertOutContains("--network\t")
- base.Cmd(gsc, "run", "--ne").AssertOutContains("--network\t")
- base.Cmd(gsc, "run", "--net", "").AssertOutContains("host\n")
- base.Cmd(gsc, "run", "-it", "--net", "").AssertOutContains("host\n")
- base.Cmd(gsc, "run", "-it", "--rm", "--net", "").AssertOutContains("host\n")
- base.Cmd(gsc, "run", "--restart", "").AssertOutContains("always\n")
- base.Cmd(gsc, "network", "rm", "").AssertNoOut("host\n") // host is unremovable
- base.Cmd(gsc, "run", "--cap-add", "").AssertOutContains("sys_admin\n")
- base.Cmd(gsc, "run", "--cap-add", "").AssertNoOut("CAP_SYS_ADMIN\n") // invalid form
-
- // Tests with an image
- base.Cmd("pull", testutil.AlpineImage).AssertOK()
- base.Cmd(gsc, "run", "-i", "").AssertOutContains(testutil.AlpineImage)
- base.Cmd(gsc, "run", "-it", "").AssertOutContains(testutil.AlpineImage)
- base.Cmd(gsc, "run", "-it", "--rm", "").AssertOutContains(testutil.AlpineImage)
-
- // Tests with an network
- testNetworkName := "nerdctl-test-completion"
- defer base.Cmd("network", "rm", testNetworkName).Run()
- base.Cmd("network", "create", testNetworkName).AssertOK()
- base.Cmd(gsc, "network", "rm", "").AssertOutContains(testNetworkName)
- base.Cmd(gsc, "run", "--net", "").AssertOutContains(testNetworkName)
-
- // Tests with raw base (without Args={"--namespace=nerdctl-test"})
- rawBase := testutil.NewBase(t)
- rawBase.Args = nil // unset "--namespace=nerdctl-test"
- rawBase.Cmd(gsc, "--cgroup-manager", "").AssertOutContains("cgroupfs\n")
- rawBase.Cmd(gsc, "").AssertOutContains("run\t")
- // mind {"--namespace=nerdctl-test"} vs {"--namespace", "nerdctl-test"}
- rawBase.Cmd(gsc, "--namespace", testutil.Namespace, "").AssertOutContains("run\t")
- rawBase.Cmd(gsc, "--namespace", testutil.Namespace, "run", "-i", "").AssertOutContains(testutil.AlpineImage)
-}
diff --git a/cmd/nerdctl/compose.go b/cmd/nerdctl/compose/compose.go
similarity index 88%
rename from cmd/nerdctl/compose.go
rename to cmd/nerdctl/compose/compose.go
index d806c57cb27..e85d31755f4 100644
--- a/cmd/nerdctl/compose.go
+++ b/cmd/nerdctl/compose/compose.go
@@ -14,24 +14,26 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
-func newComposeCommand() *cobra.Command {
+func NewComposeCommand() *cobra.Command {
var composeCommand = &cobra.Command{
Use: "compose [flags] COMMAND",
Short: "Compose",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true, // required for global short hands like -f
}
// `-f` is a nonPersistentAlias, as it conflicts with `nerdctl compose logs --follow`
- AddPersistentStringArrayFlag(composeCommand, "file", nil, []string{"f"}, nil, "", "Specify an alternate compose file")
+ helpers.AddPersistentStringArrayFlag(composeCommand, "file", nil, []string{"f"}, nil, "", "Specify an alternate compose file")
composeCommand.PersistentFlags().String("project-directory", "", "Specify an alternate working directory")
composeCommand.PersistentFlags().StringP("project-name", "p", "", "Specify an alternate project name")
composeCommand.PersistentFlags().String("env-file", "", "Specify an alternate environment file")
@@ -42,6 +44,7 @@ func newComposeCommand() *cobra.Command {
newComposeUpCommand(),
newComposeLogsCommand(),
newComposeConfigCommand(),
+ newComposeCopyCommand(),
newComposeBuildCommand(),
newComposeExecCommand(),
newComposeImagesCommand(),
@@ -67,7 +70,7 @@ func newComposeCommand() *cobra.Command {
}
func getComposeOptions(cmd *cobra.Command, debugFull, experimental bool) (composer.Options, error) {
- nerdctlCmd, nerdctlArgs := globalFlags(cmd)
+ nerdctlCmd, nerdctlArgs := helpers.GlobalFlags(cmd)
projectDirectory, err := cmd.Flags().GetString("project-directory")
if err != nil {
return composer.Options{}, err
diff --git a/cmd/nerdctl/compose_build.go b/cmd/nerdctl/compose/compose_build.go
similarity index 88%
rename from cmd/nerdctl/compose_build.go
rename to cmd/nerdctl/compose/compose_build.go
index be59dbad7b7..fbd72ff0f39 100644
--- a/cmd/nerdctl/compose_build.go
+++ b/cmd/nerdctl/compose/compose_build.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeBuildCommand() *cobra.Command {
@@ -39,7 +41,7 @@ func newComposeBuildCommand() *cobra.Command {
}
func composeBuildAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go
similarity index 95%
rename from cmd/nerdctl/compose_build_linux_test.go
rename to cmd/nerdctl/compose/compose_build_linux_test.go
index e80bdd7895f..80cc04d4c35 100644
--- a/cmd/nerdctl/compose_build_linux_test.go
+++ b/cmd/nerdctl/compose/compose_build_linux_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeBuild(t *testing.T) {
@@ -46,8 +46,8 @@ services:
dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage)
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
diff --git a/cmd/nerdctl/compose_config.go b/cmd/nerdctl/compose/compose_config.go
similarity index 90%
rename from cmd/nerdctl/compose_config.go
rename to cmd/nerdctl/compose/compose_config.go
index 90c506ce092..25a3305779f 100644
--- a/cmd/nerdctl/compose_config.go
+++ b/cmd/nerdctl/compose/compose_config.go
@@ -14,15 +14,17 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeConfigCommand() *cobra.Command {
@@ -44,7 +46,7 @@ func newComposeConfigCommand() *cobra.Command {
}
func composeConfigAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_config_test.go b/cmd/nerdctl/compose/compose_config_test.go
similarity index 77%
rename from cmd/nerdctl/compose_config_test.go
rename to cmd/nerdctl/compose/compose_config_test.go
index 4f519a46c99..18dd728da5a 100644
--- a/cmd/nerdctl/compose_config_test.go
+++ b/cmd/nerdctl/compose/compose_config_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
@@ -22,8 +22,9 @@ import (
"path/filepath"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeConfig(t *testing.T) {
@@ -68,7 +69,11 @@ services:
comp := testutil.NewComposeDir(t, fmt.Sprintf(dockerComposeYAML, "3.13"))
defer comp.CleanUp()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "config", "--hash=*").AssertOutContains("hello1")
+ // `--hash=*` is broken in Docker Compose v2.23.0: https://github.com/docker/compose/issues/11145
+ if base.Target == testutil.Nerdctl {
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "config", "--hash=*").AssertOutContains("hello1")
+ }
+
hash := base.ComposeCmd("-f", comp.YAMLFullPath(), "config", "--hash=hello1").Out()
newComp := testutil.NewComposeDir(t, fmt.Sprintf(dockerComposeYAML, "3.14"))
@@ -124,9 +129,30 @@ services:
image: alpine:3.14
`)
- base.Env = append(os.Environ(), "COMPOSE_FILE="+comp.YAMLFullPath()+","+filepath.Join(comp.Dir(), "docker-compose.test.yml"), "COMPOSE_PATH_SEPARATOR=,")
+ base.Env = append(base.Env, "COMPOSE_FILE="+comp.YAMLFullPath()+","+filepath.Join(comp.Dir(), "docker-compose.test.yml"), "COMPOSE_PATH_SEPARATOR=,")
base.ComposeCmd("config").AssertOutContains("alpine:3.14")
base.ComposeCmd("--project-directory", comp.Dir(), "config", "--services").AssertOutContainsAll("hello1\n", "hello2\n")
base.ComposeCmd("--project-directory", comp.Dir(), "config").AssertOutContains("alpine:3.14")
}
+
+func TestComposeConfigWithEnvFile(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ const dockerComposeYAML = `
+services:
+ hello:
+ image: ${image}
+`
+
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ envFile := filepath.Join(comp.Dir(), "env")
+ const envFileContent = `
+image: hello-world
+`
+ assert.NilError(t, os.WriteFile(envFile, []byte(envFileContent), 0644))
+
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "--env-file", envFile, "config").AssertOutContains("image: hello-world")
+}
diff --git a/cmd/nerdctl/compose/compose_cp.go b/cmd/nerdctl/compose/compose_cp.go
new file mode 100644
index 00000000000..a35078bc405
--- /dev/null
+++ b/cmd/nerdctl/compose/compose_cp.go
@@ -0,0 +1,105 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package compose
+
+import (
+ "errors"
+
+ "github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+)
+
+func newComposeCopyCommand() *cobra.Command {
+ usage := `cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
+ nerdctl compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH`
+ var composeCpCommand = &cobra.Command{
+ Use: usage,
+ Short: "Copy files/folders between a service container and the local filesystem",
+ Args: cobra.ExactArgs(2),
+ RunE: composeCopyAction,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+ composeCpCommand.Flags().Bool("dry-run", false, "Execute command in dry run mode")
+ composeCpCommand.Flags().BoolP("follow-link", "L", false, "Always follow symbol link in SRC_PATH")
+ composeCpCommand.Flags().Int("index", 0, "index of the container if service has multiple replicas")
+ return composeCpCommand
+}
+
+func composeCopyAction(cmd *cobra.Command, args []string) error {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return err
+ }
+ source := args[0]
+ if source == "" {
+ return errors.New("source can not be empty")
+ }
+ destination := args[1]
+ if destination == "" {
+ return errors.New("destination can not be empty")
+ }
+
+ dryRun, err := cmd.Flags().GetBool("dry-run")
+ if err != nil {
+ return err
+ }
+ followLink, err := cmd.Flags().GetBool("follow-link")
+ if err != nil {
+ return err
+ }
+ index, err := cmd.Flags().GetInt("index")
+ if err != nil {
+ return err
+ }
+ address := globalOptions.Address
+ // rootless cp runs in the host namespaces, so the address is different
+ if rootlessutil.IsRootless() {
+ address, err = rootlessutil.RootlessContainredSockAddress()
+ if err != nil {
+ return err
+ }
+ }
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
+ options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental)
+ if err != nil {
+ return err
+ }
+ c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr())
+ if err != nil {
+ return err
+ }
+
+ co := composer.CopyOptions{
+ Source: source,
+ Destination: destination,
+ Index: index,
+ FollowLink: followLink,
+ DryRun: dryRun,
+ }
+ return c.Copy(ctx, co)
+
+}
diff --git a/cmd/nerdctl/compose/compose_cp_linux_test.go b/cmd/nerdctl/compose/compose_cp_linux_test.go
new file mode 100644
index 00000000000..605210d8946
--- /dev/null
+++ b/cmd/nerdctl/compose/compose_cp_linux_test.go
@@ -0,0 +1,69 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package compose
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestComposeCopy(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ var dockerComposeYAML = fmt.Sprintf(`
+version: '3.1'
+
+services:
+ svc0:
+ image: %s
+ command: "sleep infinity"
+`, testutil.CommonImage)
+
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+ projectName := comp.ProjectName()
+ t.Logf("projectName=%q", projectName)
+
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
+ defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
+
+ // gernetate test file
+ srcDir := t.TempDir()
+ srcFile := filepath.Join(srcDir, "test-file")
+ srcFileContent := []byte("test-file-content")
+ err := os.WriteFile(srcFile, srcFileContent, 0o644)
+ assert.NilError(t, err)
+
+ // test copy to service
+ destPath := "/dest-no-exist-no-slash"
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "cp", srcFile, "svc0:"+destPath).AssertOK()
+
+ // test copy from service
+ destFile := filepath.Join(srcDir, "test-file2")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "cp", "svc0:"+destPath, destFile).AssertOK()
+
+ destFileContent, err := os.ReadFile(destFile)
+ assert.NilError(t, err)
+ assert.DeepEqual(t, srcFileContent, destFileContent)
+
+}
diff --git a/cmd/nerdctl/compose_create.go b/cmd/nerdctl/compose/compose_create.go
similarity index 91%
rename from cmd/nerdctl/compose_create.go
rename to cmd/nerdctl/compose/compose_create.go
index 7b60ed8dc98..9621398ae49 100644
--- a/cmd/nerdctl/compose_create.go
+++ b/cmd/nerdctl/compose/compose_create.go
@@ -14,15 +14,17 @@
limitations under the License.
*/
-package main
+package compose
import (
"errors"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeCreateCommand() *cobra.Command {
@@ -42,7 +44,7 @@ func newComposeCreateCommand() *cobra.Command {
}
func composeCreateAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -63,7 +65,7 @@ func composeCreateAction(cmd *cobra.Command, args []string) error {
}
noRecreate, err := cmd.Flags().GetBool("no-recreate")
if err != nil {
- return nil
+ return err
}
if forceRecreate && noRecreate {
return errors.New("flag --force-recreate and --no-recreate cannot be specified together")
diff --git a/cmd/nerdctl/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go
similarity index 77%
rename from cmd/nerdctl/compose_create_linux_test.go
rename to cmd/nerdctl/compose/compose_create_linux_test.go
index 7935d437c36..43f2dc40067 100644
--- a/cmd/nerdctl/compose_create_linux_test.go
+++ b/cmd/nerdctl/compose/compose_create_linux_test.go
@@ -14,20 +14,16 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeCreate(t *testing.T) {
- // docker-compose v1 depecreated this command
- // docker-compose v2 reimplemented this command
- testutil.DockerIncompatible(t)
-
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -46,16 +42,12 @@ services:
// 1.1 `compose create` should create service container (in `created` status)
base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
// 1.2 created container can be started by `compose start`
base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK()
}
func TestComposeCreateDependency(t *testing.T) {
- // docker-compose v1 depecreated this command
- // docker-compose v2 reimplemented this command
- testutil.DockerIncompatible(t)
-
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -78,14 +70,11 @@ services:
// `compose create` should create containers for both services and their dependencies
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "svc0").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Created", "created")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1", "-a").AssertOutContainsAny("Created", "created")
}
func TestComposeCreatePull(t *testing.T) {
- // docker-compose v1 depecreated this command
- // docker-compose v2 reimplemented this command
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
@@ -111,14 +100,10 @@ services:
base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK()
base.Cmd("rmi", "-f", testutil.AlpineImage).Run()
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "always").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
}
func TestComposeCreateBuild(t *testing.T) {
- // docker-compose v1 depecreated this command
- // docker-compose v2 reimplemented this command
- testutil.DockerIncompatible(t)
-
const imageSvc0 = "composebuild_svc0"
dockerComposeYAML := fmt.Sprintf(`
@@ -131,8 +116,8 @@ services:
dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage)
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
@@ -148,5 +133,5 @@ services:
// `compose create --build` should succeed: image is built and container is created
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--build").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "svc0").AssertOutContains(imageSvc0)
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
}
diff --git a/cmd/nerdctl/compose_down.go b/cmd/nerdctl/compose/compose_down.go
similarity index 88%
rename from cmd/nerdctl/compose_down.go
rename to cmd/nerdctl/compose/compose_down.go
index 236f96fd61a..30552d18947 100644
--- a/cmd/nerdctl/compose_down.go
+++ b/cmd/nerdctl/compose/compose_down.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeDownCommand() *cobra.Command {
@@ -38,7 +40,7 @@ func newComposeDownCommand() *cobra.Command {
}
func composeDownAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_down_linux_test.go b/cmd/nerdctl/compose/compose_down_linux_test.go
similarity index 89%
rename from cmd/nerdctl/compose_down_linux_test.go
rename to cmd/nerdctl/compose/compose_down_linux_test.go
index 5b3943f3782..b995631d6b6 100644
--- a/cmd/nerdctl/compose_down_linux_test.go
+++ b/cmd/nerdctl/compose/compose_down_linux_test.go
@@ -14,23 +14,20 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeDownRemoveUsedNetwork(t *testing.T) {
base := testutil.NewBase(t)
- // The error output is different with docker
- testutil.DockerIncompatible(t)
-
var (
dockerComposeYAMLOrphan = fmt.Sprintf(`
version: '3.1'
@@ -60,7 +57,7 @@ services:
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "up", "-d").AssertOK()
defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "--remove-orphans").AssertOK()
- base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "-v").AssertCombinedOutContains("is in use")
+ base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "-v").AssertCombinedOutContains("in use")
}
@@ -99,5 +96,5 @@ services:
defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run()
base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "--remove-orphans").AssertOK()
- base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutNotContains(orphanContainer)
+ base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps", "-a").AssertOutNotContains(orphanContainer)
}
diff --git a/cmd/nerdctl/compose_exec.go b/cmd/nerdctl/compose/compose_exec.go
similarity index 78%
rename from cmd/nerdctl/compose_exec.go
rename to cmd/nerdctl/compose/compose_exec.go
index 06919b6309e..79d5da5bdcb 100644
--- a/cmd/nerdctl/compose_exec.go
+++ b/cmd/nerdctl/compose/compose_exec.go
@@ -14,15 +14,19 @@
limitations under the License.
*/
-package main
+package compose
import (
"errors"
+ "os"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
+ "github.com/moby/term"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeExecCommand() *cobra.Command {
@@ -36,22 +40,29 @@ func newComposeExecCommand() *cobra.Command {
}
composeExecCommand.Flags().SetInterspersed(false)
- composeExecCommand.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY")
- composeExecCommand.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached")
+ _, isTerminal := term.GetFdInfo(os.Stdout)
+ composeExecCommand.Flags().BoolP("no-TTY", "T", !isTerminal, "Disable pseudo-TTY allocation. By default nerdctl compose exec allocates a TTY.")
composeExecCommand.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background")
composeExecCommand.Flags().StringP("workdir", "w", "", "Working directory inside the container")
// env needs to be StringArray, not StringSlice, to prevent "FOO=foo1,foo2" from being split to {"FOO=foo1", "foo2"}
composeExecCommand.Flags().StringArrayP("env", "e", nil, "Set environment variables")
- // TODO: no-TTY flag
composeExecCommand.Flags().Bool("privileged", false, "Give extended privileges to the command")
composeExecCommand.Flags().StringP("user", "u", "", "Username or UID (format: [:])")
composeExecCommand.Flags().Int("index", 1, "index of the container if the service has multiple instances.")
+ composeExecCommand.Flags().BoolP("interactive", "i", true, "Keep STDIN open even if not attached")
+ composeExecCommand.Flags().MarkHidden("interactive")
+ // The -t does not has effect to keep the compatibility with docker.
+ // The proposal of -t is to keep "muscle memory" with compose v1: https://github.com/docker/compose/issues/9207
+ // FYI: https://github.com/docker/compose/blob/v2.23.1/cmd/compose/exec.go#L77
+ composeExecCommand.Flags().BoolP("tty", "t", true, "Allocate a pseudo-TTY")
+ composeExecCommand.Flags().MarkHidden("tty")
+
return composeExecCommand
}
func composeExecAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -59,7 +70,7 @@ func composeExecAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
- tty, err := cmd.Flags().GetBool("tty")
+ noTty, err := cmd.Flags().GetBool("no-TTY")
if err != nil {
return err
}
@@ -96,8 +107,8 @@ func composeExecAction(cmd *cobra.Command, args []string) error {
return errors.New("currently flag -i and -d cannot be specified together (FIXME)")
}
// https://github.com/containerd/nerdctl/blob/v1.0.0/cmd/nerdctl/exec.go#L122
- if tty && detach {
- return errors.New("currently flag -t and -d cannot be specified together (FIXME)")
+ if !noTty && detach {
+ return errors.New("currently flag -d should be specified with --no-TTY (FIXME)")
}
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
@@ -119,7 +130,7 @@ func composeExecAction(cmd *cobra.Command, args []string) error {
Index: index,
Interactive: interactive,
- Tty: tty,
+ Tty: !noTty,
Detach: detach,
WorkDir: workdir,
Env: env,
diff --git a/cmd/nerdctl/compose_exec_linux_test.go b/cmd/nerdctl/compose/compose_exec_linux_test.go
similarity index 66%
rename from cmd/nerdctl/compose_exec_linux_test.go
rename to cmd/nerdctl/compose/compose_exec_linux_test.go
index 28d92d76efa..24e0b3a51de 100644
--- a/cmd/nerdctl/compose_exec_linux_test.go
+++ b/cmd/nerdctl/compose/compose_exec_linux_test.go
@@ -14,23 +14,21 @@
limitations under the License.
*/
-package main
+package compose
import (
"errors"
"fmt"
- "os"
- "runtime"
+ "net"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeExec(t *testing.T) {
- // disabling `-it` in `compose exec` is only supported in compose v2.
- // Currently CI is using compose v1.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -53,16 +51,13 @@ services:
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
// test basic functionality and `--workdir` flag
- base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "-t=false", "svc0", "echo", "success").AssertOutExactly("success\n")
- base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "-t=false", "--workdir", "/tmp", "svc0", "pwd").AssertOutExactly("/tmp\n")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "svc0", "echo", "success").AssertOutExactly("success\n")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "--workdir", "/tmp", "svc0", "pwd").AssertOutExactly("/tmp\n")
// cannot `exec` on non-running service
base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "svc1", "echo", "success").AssertFail()
}
func TestComposeExecWithEnv(t *testing.T) {
- // disabling `-it` in `compose exec` is only supported in compose v2.
- // Currently CI is using compose v1.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -82,8 +77,8 @@ services:
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
// FYI: https://github.com/containerd/nerdctl/blob/e4b2b6da56555dc29ed66d0fd8e7094ff2bc002d/cmd/nerdctl/run_test.go#L177
- base.Env = append(os.Environ(), "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
- base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "-t=false",
+ base.Env = append(base.Env, "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY",
"--env", "FOO=foo1,foo2",
"--env", "BAR=bar1 bar2",
"--env", "BAZ=",
@@ -102,7 +97,7 @@ services:
if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") {
return errors.New("got bad BAR")
}
- if !strings.Contains(stdout, "\nBAZ=\n") && runtime.GOOS != "windows" {
+ if !strings.Contains(stdout, "\nBAZ=\n") {
return errors.New("got bad BAZ")
}
if strings.Contains(stdout, "QUX") {
@@ -117,10 +112,10 @@ services:
if !strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n") {
return errors.New("got bad GRAULT")
}
- if !strings.Contains(stdout, "\nGARPLY=\n") && runtime.GOOS != "windows" {
+ if !strings.Contains(stdout, "\nGARPLY=\n") {
return errors.New("got bad GARPLY")
}
- if !strings.Contains(stdout, "\nWALDO=\n") && runtime.GOOS != "windows" {
+ if !strings.Contains(stdout, "\nWALDO=\n") {
return errors.New("got bad WALDO")
}
@@ -129,9 +124,6 @@ services:
}
func TestComposeExecWithUser(t *testing.T) {
- // disabling `-it` in `compose exec` is only supported in compose v2.
- // Currently CI is using compose v1.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -160,7 +152,7 @@ services:
}
for userStr, expected := range testCases {
- args := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "-t=false"}
+ args := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY"}
if userStr != "" {
args = append(args, "--user", userStr)
}
@@ -171,8 +163,6 @@ services:
func TestComposeExecTTY(t *testing.T) {
// `-i` in `compose run & exec` is only supported in compose v2.
- // Currently CI is using compose v1.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
if testutil.GetTarget() == testutil.Nerdctl {
testutil.RequireDaemonVersion(base, ">= 1.6.0-0")
@@ -204,6 +194,96 @@ services:
unbuffer := []string{"unbuffer"}
base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "svc0", "stty").AssertOutContains(sttyPartialOutput) // `-it`
base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "svc0", "stty").AssertOutContains(sttyPartialOutput) // `-t`
- base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-t=false", "svc0", "stty").AssertFail() // `-i`
- base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "-t=false", "svc0", "stty").AssertFail()
+ base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "--no-TTY", "svc0", "stty").AssertFail() // `-i`
+ base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "svc0", "stty").AssertFail()
+}
+
+func TestComposeExecWithIndex(t *testing.T) {
+ base := testutil.NewBase(t)
+ var dockerComposeYAML = fmt.Sprintf(`
+version: '3.1'
+
+services:
+ svc0:
+ image: %s
+ command: "sleep infinity"
+ deploy:
+ replicas: 3
+`, testutil.CommonImage)
+
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ t.Cleanup(func() {
+ comp.CleanUp()
+ })
+ projectName := comp.ProjectName()
+ t.Logf("projectName=%q", projectName)
+
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "svc0").AssertOK()
+ t.Cleanup(func() {
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
+ })
+
+ // try 5 times to ensure that results are stable
+ for i := 0; i < 5; i++ {
+ for _, j := range []string{"1", "2", "3"} {
+ name := fmt.Sprintf("%s-svc0-%s", projectName, j)
+ host := fmt.Sprintf("%s.%s_default", name, projectName)
+ var (
+ expectIP string
+ realIP string
+ )
+ // docker and nerdctl have different DNS resolution behaviors.
+ // it uses the ID in the /etc/hosts file, so we need to fetch the ID first.
+ if testutil.GetTarget() == testutil.Docker {
+ base.Cmd("ps", "--filter", fmt.Sprintf("name=%s", name), "--format", "{{.ID}}").AssertOutWithFunc(func(stdout string) error {
+ host = strings.TrimSpace(stdout)
+ return nil
+ })
+ }
+ cmds := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "--index", j, "svc0"}
+ base.ComposeCmd(append(cmds, "cat", "/etc/hosts")...).
+ AssertOutWithFunc(func(stdout string) error {
+ lines := strings.Split(stdout, "\n")
+ for _, line := range lines {
+ if !strings.Contains(line, host) {
+ continue
+ }
+ fields := strings.Fields(line)
+ if len(fields) == 0 {
+ continue
+ }
+ expectIP = fields[0]
+ return nil
+ }
+ return errors.New("fail to get the expected ip address")
+ })
+ base.ComposeCmd(append(cmds, "ip", "addr", "show", "dev", "eth0")...).
+ AssertOutWithFunc(func(stdout string) error {
+ ip := findIP(stdout)
+ if ip == nil {
+ return errors.New("fail to get the real ip address")
+ }
+ realIP = ip.String()
+ return nil
+ })
+ assert.Equal(t, realIP, expectIP)
+ }
+ }
+}
+
+func findIP(output string) net.IP {
+ var ip string
+ lines := strings.Split(output, "\n")
+ for _, line := range lines {
+ if !strings.Contains(line, "inet ") {
+ continue
+ }
+ fields := strings.Fields(line)
+ if len(fields) <= 1 {
+ continue
+ }
+ ip = strings.Split(fields[1], "/")[0]
+ break
+ }
+ return net.ParseIP(ip)
}
diff --git a/cmd/nerdctl/compose_images.go b/cmd/nerdctl/compose/compose_images.go
similarity index 89%
rename from cmd/nerdctl/compose_images.go
rename to cmd/nerdctl/compose/compose_images.go
index 36816769a0f..30212559e1d 100644
--- a/cmd/nerdctl/compose_images.go
+++ b/cmd/nerdctl/compose/compose_images.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"context"
@@ -22,17 +22,20 @@ import (
"strings"
"text/tabwriter"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/containerd/snapshots"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/containerd/v2/pkg/progress"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func newComposeImagesCommand() *cobra.Command {
@@ -49,7 +52,7 @@ func newComposeImagesCommand() *cobra.Command {
}
func composeImagesAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_images_linux_test.go b/cmd/nerdctl/compose/compose_images_linux_test.go
similarity index 96%
rename from cmd/nerdctl/compose_images_linux_test.go
rename to cmd/nerdctl/compose/compose_images_linux_test.go
index 85132a6890c..f9f7f475186 100644
--- a/cmd/nerdctl/compose_images_linux_test.go
+++ b/cmd/nerdctl/compose/compose_images_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"encoding/json"
@@ -22,7 +22,7 @@ import (
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeImages(t *testing.T) {
@@ -78,10 +78,6 @@ volumes:
}
func TestComposeImagesJson(t *testing.T) {
- // `--format` is only supported in docker compose v2.
- // Currently, CI is using docker compose v1.
- testutil.DockerIncompatible(t)
-
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
diff --git a/cmd/nerdctl/compose_kill.go b/cmd/nerdctl/compose/compose_kill.go
similarity index 85%
rename from cmd/nerdctl/compose_kill.go
rename to cmd/nerdctl/compose/compose_kill.go
index 1e483273ff1..b1adcf4c753 100644
--- a/cmd/nerdctl/compose_kill.go
+++ b/cmd/nerdctl/compose/compose_kill.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeKillCommand() *cobra.Command {
@@ -36,7 +38,7 @@ func newComposeKillCommand() *cobra.Command {
}
func composeKillAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_kill_linux_test.go b/cmd/nerdctl/compose/compose_kill_linux_test.go
similarity index 84%
rename from cmd/nerdctl/compose_kill_linux_test.go
rename to cmd/nerdctl/compose/compose_kill_linux_test.go
index 3d948ebadb8..6571950a62e 100644
--- a/cmd/nerdctl/compose_kill_linux_test.go
+++ b/cmd/nerdctl/compose/compose_kill_linux_test.go
@@ -14,21 +14,17 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeKill(t *testing.T) {
- // docker-compose v2 hides exited/killed containers in `compose ps`, and shows
- // them if `-a` is passed, which is not supported yet by `nerdctl compose`.
- testutil.DockerIncompatible(t)
-
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -73,6 +69,6 @@ volumes:
base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "db").AssertOK()
time.Sleep(3 * time.Second)
// Docker Compose v1: "Exit 137", v2: "exited (137)"
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny(" 137", "(137)")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny(" 137", "(137)")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")
}
diff --git a/cmd/nerdctl/compose_logs.go b/cmd/nerdctl/compose/compose_logs.go
similarity index 89%
rename from cmd/nerdctl/compose_logs.go
rename to cmd/nerdctl/compose/compose_logs.go
index 79902f7bad2..5a6fd723cc3 100644
--- a/cmd/nerdctl/compose_logs.go
+++ b/cmd/nerdctl/compose/compose_logs.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeLogsCommand() *cobra.Command {
@@ -40,7 +42,7 @@ func newComposeLogsCommand() *cobra.Command {
}
func composeLogsAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_pause.go b/cmd/nerdctl/compose/compose_pause.go
similarity index 90%
rename from cmd/nerdctl/compose_pause.go
rename to cmd/nerdctl/compose/compose_pause.go
index 429b7d853b0..9fd02bf3fa7 100644
--- a/cmd/nerdctl/compose_pause.go
+++ b/cmd/nerdctl/compose/compose_pause.go
@@ -14,12 +14,14 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
)
func newComposePauseCommand() *cobra.Command {
@@ -35,7 +37,7 @@ func newComposePauseCommand() *cobra.Command {
}
func composePauseAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -70,7 +72,7 @@ func newComposeUnpauseCommand() *cobra.Command {
}
func composeUnpauseAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_pause_linux_test.go b/cmd/nerdctl/compose/compose_pause_linux_test.go
similarity index 91%
rename from cmd/nerdctl/compose_pause_linux_test.go
rename to cmd/nerdctl/compose/compose_pause_linux_test.go
index aaccca155bd..381e8686d6b 100644
--- a/cmd/nerdctl/compose_pause_linux_test.go
+++ b/cmd/nerdctl/compose/compose_pause_linux_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposePauseAndUnpause(t *testing.T) {
@@ -52,7 +52,7 @@ services:
// pause a service should (only) pause its own container
base.ComposeCmd("-f", comp.YAMLFullPath(), "pause", "svc0").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Paused", "paused")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Paused", "paused")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Up", "running")
// unpause should be able to recover the paused service container
diff --git a/cmd/nerdctl/compose_port.go b/cmd/nerdctl/compose/compose_port.go
similarity index 89%
rename from cmd/nerdctl/compose_port.go
rename to cmd/nerdctl/compose/compose_port.go
index 92c3ea3189a..2cbf14b0287 100644
--- a/cmd/nerdctl/compose_port.go
+++ b/cmd/nerdctl/compose/compose_port.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"strconv"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposePortCommand() *cobra.Command {
@@ -42,7 +44,7 @@ func newComposePortCommand() *cobra.Command {
}
func composePortAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_port_linux_test.go b/cmd/nerdctl/compose/compose_port_linux_test.go
similarity index 89%
rename from cmd/nerdctl/compose_port_linux_test.go
rename to cmd/nerdctl/compose/compose_port_linux_test.go
index f5240edc55b..15946557ad2 100644
--- a/cmd/nerdctl/compose_port_linux_test.go
+++ b/cmd/nerdctl/compose/compose_port_linux_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposePort(t *testing.T) {
@@ -52,10 +52,6 @@ services:
}
func TestComposePortFailure(t *testing.T) {
- // when no port mapping is found, docker compose v1 prints `\n` while v2 prints `:0\n`
- // both v1 and v2 have exit code 0 (succeess)
- // nerdctl compose will fail with error (no public port found).
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
diff --git a/cmd/nerdctl/compose_ps.go b/cmd/nerdctl/compose/compose_ps.go
similarity index 59%
rename from cmd/nerdctl/compose_ps.go
rename to cmd/nerdctl/compose/compose_ps.go
index 54f0ba1e596..1626b14cd5f 100644
--- a/cmd/nerdctl/compose_ps.go
+++ b/cmd/nerdctl/compose/compose_ps.go
@@ -14,24 +14,31 @@
limitations under the License.
*/
-package main
+package compose
import (
"context"
"fmt"
+ "strings"
"text/tabwriter"
+ "time"
- "github.com/containerd/containerd"
- gocni "github.com/containerd/go-cni"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/portutil"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/runtime/restart"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/go-cni"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/portutil"
)
func newComposePsCommand() *cobra.Command {
@@ -42,7 +49,12 @@ func newComposePsCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
- composePsCommand.Flags().String("format", "", "Format the output. Supported values: [json]")
+ composePsCommand.Flags().String("format", "table", "Format the output. Supported values: [table|json]")
+ composePsCommand.Flags().String("filter", "", "Filter matches containers based on given conditions")
+ composePsCommand.Flags().StringArray("status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
+ composePsCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs")
+ composePsCommand.Flags().Bool("services", false, "Display services")
+ composePsCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
return composePsCommand
}
@@ -63,7 +75,7 @@ type composeContainerPrintable struct {
}
func composePsAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -71,8 +83,40 @@ func composePsAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
- if format != "json" && format != "" {
- return fmt.Errorf("unsupported format %s, supported formats are: [json]", format)
+ if format != "json" && format != "table" {
+ return fmt.Errorf("unsupported format %s, supported formats are: [table|json]", format)
+ }
+ status, err := cmd.Flags().GetStringArray("status")
+ if err != nil {
+ return err
+ }
+ quiet, err := cmd.Flags().GetBool("quiet")
+ if err != nil {
+ return err
+ }
+ displayServices, err := cmd.Flags().GetBool("services")
+ if err != nil {
+ return err
+ }
+ filter, err := cmd.Flags().GetString("filter")
+ if err != nil {
+ return err
+ }
+ if filter != "" {
+ splited := strings.SplitN(filter, "=", 2)
+ if len(splited) != 2 {
+ return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter)
+ }
+ // currently only the 'status' filter is supported
+ if splited[0] != "status" {
+ return fmt.Errorf("invalid filter '%s'", splited[0])
+ }
+ status = append(status, splited[1])
+ }
+
+ all, err := cmd.Flags().GetBool("all")
+ if err != nil {
+ return err
}
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
@@ -97,6 +141,41 @@ func composePsAction(cmd *cobra.Command, args []string) error {
return err
}
+ if !all {
+ var upContainers []containerd.Container
+ for _, container := range containers {
+ // cStatus := formatter.ContainerStatus(ctx, c)
+ cStatus, err := containerutil.ContainerStatus(ctx, container)
+ if err != nil {
+ continue
+ }
+ if cStatus.Status == containerd.Running {
+ upContainers = append(upContainers, container)
+ }
+ }
+ containers = upContainers
+ }
+
+ if len(status) != 0 {
+ var filterdContainers []containerd.Container
+ for _, container := range containers {
+ cStatus := statusForFilter(ctx, container)
+ for _, s := range status {
+ if cStatus == s {
+ filterdContainers = append(filterdContainers, container)
+ }
+ }
+ }
+ containers = filterdContainers
+ }
+
+ if quiet {
+ for _, c := range containers {
+ fmt.Fprintln(cmd.OutOrStdout(), c.ID())
+ }
+ return nil
+ }
+
containersPrintable := make([]composeContainerPrintable, len(containers))
eg, ctx := errgroup.WithContext(ctx)
for i, container := range containers {
@@ -121,6 +200,12 @@ func composePsAction(cmd *cobra.Command, args []string) error {
return err
}
+ if displayServices {
+ for _, p := range containersPrintable {
+ fmt.Fprintln(cmd.OutOrStdout(), p.Service)
+ }
+ return nil
+ }
if format == "json" {
outJSON, err := formatter.ToJSON(containersPrintable, "", "")
if err != nil {
@@ -178,7 +263,7 @@ func composeContainerPrintableTab(ctx context.Context, container containerd.Cont
}, nil
}
-// composeContainerPrintableTab constructs composeContainerPrintable with fields
+// composeContainerPrintableJSON constructs composeContainerPrintable with fields
// only for json output and compatible docker output.
func composeContainerPrintableJSON(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) {
info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
@@ -198,9 +283,11 @@ func composeContainerPrintableJSON(ctx context.Context, container containerd.Con
if err == nil {
// show exitCode only when container is exited/stopped
if status.Status == containerd.Stopped {
+ state = "exited"
exitCode = status.ExitStatus
+ } else {
+ state = string(status.Status)
}
- state = string(status.Status)
} else {
state = string(containerd.Unknown)
}
@@ -236,7 +323,7 @@ type PortPublisher struct {
// formatPublishers parses and returns docker-compatible []PortPublisher from
// label map. If an error happens, an empty slice is returned.
func formatPublishers(labelMap map[string]string) []PortPublisher {
- mapper := func(pm gocni.PortMapping) PortPublisher {
+ mapper := func(pm cni.PortMapping) PortPublisher {
return PortPublisher{
URL: pm.HostIP,
TargetPort: int(pm.ContainerPort),
@@ -251,7 +338,45 @@ func formatPublishers(labelMap map[string]string) []PortPublisher {
dockerPorts = append(dockerPorts, mapper(p))
}
} else {
- logrus.Error(err.Error())
+ log.L.Error(err.Error())
}
return dockerPorts
}
+
+// statusForFilter returns the status value to be matched with the 'status' filter
+func statusForFilter(ctx context.Context, c containerd.Container) string {
+ // Just in case, there is something wrong in server.
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ task, err := c.Task(ctx, nil)
+ if err != nil {
+ // NOTE: NotFound doesn't mean that container hasn't started.
+ // In docker/CRI-containerd plugin, the task will be deleted
+ // when it exits. So, the status will be "created" for this
+ // case.
+ if errdefs.IsNotFound(err) {
+ return string(containerd.Created)
+ }
+ return string(containerd.Unknown)
+ }
+
+ status, err := task.Status(ctx)
+ if err != nil {
+ return string(containerd.Unknown)
+ }
+ labels, err := c.Labels(ctx)
+ if err != nil {
+ return string(containerd.Unknown)
+ }
+
+ switch s := status.Status; s {
+ case containerd.Stopped:
+ if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(status, labels) {
+ return "restarting"
+ }
+ return "exited"
+ default:
+ return string(s)
+ }
+}
diff --git a/cmd/nerdctl/compose_ps_linux_test.go b/cmd/nerdctl/compose/compose_ps_linux_test.go
similarity index 56%
rename from cmd/nerdctl/compose_ps_linux_test.go
rename to cmd/nerdctl/compose/compose_ps_linux_test.go
index 7fdda4a47ae..df6f1d3cfe5 100644
--- a/cmd/nerdctl/compose_ps_linux_test.go
+++ b/cmd/nerdctl/compose/compose_ps_linux_test.go
@@ -14,20 +14,100 @@
limitations under the License.
*/
-package main
+package compose
import (
"encoding/json"
"fmt"
"strings"
"testing"
+ "time"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/tabutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
+func TestComposePs(t *testing.T) {
+ base := testutil.NewBase(t)
+ var dockerComposeYAML = fmt.Sprintf(`
+version: '3.1'
+
+services:
+ wordpress:
+ image: %s
+ container_name: wordpress_container
+ ports:
+ - 8080:80
+ environment:
+ WORDPRESS_DB_HOST: db
+ WORDPRESS_DB_USER: exampleuser
+ WORDPRESS_DB_PASSWORD: examplepass
+ WORDPRESS_DB_NAME: exampledb
+ volumes:
+ - wordpress:/var/www/html
+ db:
+ image: %s
+ container_name: db_container
+ environment:
+ MYSQL_DATABASE: exampledb
+ MYSQL_USER: exampleuser
+ MYSQL_PASSWORD: examplepass
+ MYSQL_RANDOM_ROOT_PASSWORD: '1'
+ volumes:
+ - db:/var/lib/mysql
+ alpine:
+ image: %s
+ container_name: alpine_container
+
+volumes:
+ wordpress:
+ db:
+`, testutil.WordpressImage, testutil.MariaDBImage, testutil.AlpineImage)
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+ projectName := comp.ProjectName()
+ t.Logf("projectName=%q", projectName)
+
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
+ defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
+
+ assertHandler := func(expectedName, expectedImage string) func(stdout string) error {
+ return func(stdout string) error {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ if len(lines) < 2 {
+ return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
+ }
+
+ tab := tabutil.NewReader("NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS")
+ err := tab.ParseHeader(lines[0])
+ if err != nil {
+ return fmt.Errorf("failed to parse header: %v", err)
+ }
+
+ container, _ := tab.ReadRow(lines[1], "NAME")
+ assert.Equal(t, container, expectedName)
+
+ image, _ := tab.ReadRow(lines[1], "IMAGE")
+ assert.Equal(t, image, expectedImage)
+
+ return nil
+ }
+
+ }
+
+ time.Sleep(3 * time.Second)
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutWithFunc(assertHandler("wordpress_container", testutil.WordpressImage))
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutWithFunc(assertHandler("db_container", testutil.MariaDBImage))
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.AlpineImage)
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage))
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage))
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--services", "-a").AssertOutContainsAll("wordpress\n", "db\n", "alpine\n")
+}
+
func TestComposePsJSON(t *testing.T) {
- // `--format` is only supported in docker compose v2.
- // Currently, CI is using docker compose v1.
+ // docker parses unknown 'format' as a Go template and won't output an error
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
@@ -101,8 +181,8 @@ volumes:
AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"running"`, `"TargetPort":80`, `"PublishedPort":8080`))
// check wordpress is stopped
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress").
- AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"stopped"`))
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress", "-a").
+ AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"exited"`))
// check wordpress is removed
base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress").
diff --git a/cmd/nerdctl/compose_pull.go b/cmd/nerdctl/compose/compose_pull.go
similarity index 85%
rename from cmd/nerdctl/compose_pull.go
rename to cmd/nerdctl/compose/compose_pull.go
index 9d004353fc5..9763377d4a5 100644
--- a/cmd/nerdctl/compose_pull.go
+++ b/cmd/nerdctl/compose/compose_pull.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposePullCommand() *cobra.Command {
@@ -36,7 +38,7 @@ func newComposePullCommand() *cobra.Command {
}
func composePullAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_pull_linux_test.go b/cmd/nerdctl/compose/compose_pull_linux_test.go
similarity index 94%
rename from cmd/nerdctl/compose_pull_linux_test.go
rename to cmd/nerdctl/compose/compose_pull_linux_test.go
index d350892f1b3..64e267baa24 100644
--- a/cmd/nerdctl/compose_pull_linux_test.go
+++ b/cmd/nerdctl/compose/compose_pull_linux_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposePullWithService(t *testing.T) {
@@ -62,5 +62,5 @@ volumes:
projectName := comp.ProjectName()
t.Logf("projectName=%q", projectName)
- base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "db").AssertNoOut("wordpress")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "pull", "db").AssertOutNotContains("wordpress")
}
diff --git a/cmd/nerdctl/compose_push.go b/cmd/nerdctl/compose/compose_push.go
similarity index 84%
rename from cmd/nerdctl/compose_push.go
rename to cmd/nerdctl/compose/compose_push.go
index 2c899dabf39..50063d556e9 100644
--- a/cmd/nerdctl/compose_push.go
+++ b/cmd/nerdctl/compose/compose_push.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposePushCommand() *cobra.Command {
@@ -35,7 +37,7 @@ func newComposePushCommand() *cobra.Command {
}
func composePushAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_restart.go b/cmd/nerdctl/compose/compose_restart.go
similarity index 86%
rename from cmd/nerdctl/compose_restart.go
rename to cmd/nerdctl/compose/compose_restart.go
index adb47e3dee4..0ee3a936cf1 100644
--- a/cmd/nerdctl/compose_restart.go
+++ b/cmd/nerdctl/compose/compose_restart.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeRestartCommand() *cobra.Command {
@@ -36,7 +38,7 @@ func newComposeRestartCommand() *cobra.Command {
}
func composeRestartAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_restart_linux_test.go b/cmd/nerdctl/compose/compose_restart_linux_test.go
similarity index 84%
rename from cmd/nerdctl/compose_restart_linux_test.go
rename to cmd/nerdctl/compose/compose_restart_linux_test.go
index 8de3513fd41..6d5fe1fdedc 100644
--- a/cmd/nerdctl/compose_restart_linux_test.go
+++ b/cmd/nerdctl/compose/compose_restart_linux_test.go
@@ -14,19 +14,16 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeRestart(t *testing.T) {
- // docker-compose v2 hides exited containers in `compose ps`, and shows
- // them if `-a` is passed, which is not supported yet by `nerdctl compose`.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -68,13 +65,13 @@ volumes:
// stop and restart a single service.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "db").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running")
// stop one service and restart all (also check `--timeout` arg).
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "--timeout", "5").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")
diff --git a/cmd/nerdctl/compose_rm.go b/cmd/nerdctl/compose/compose_rm.go
similarity index 84%
rename from cmd/nerdctl/compose_rm.go
rename to cmd/nerdctl/compose/compose_rm.go
index 6ac686f6b86..d82e345db76 100644
--- a/cmd/nerdctl/compose_rm.go
+++ b/cmd/nerdctl/compose/compose_rm.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"strings"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeRemoveCommand() *cobra.Command {
@@ -41,7 +43,7 @@ func newComposeRemoveCommand() *cobra.Command {
}
func composeRemoveAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -50,18 +52,15 @@ func composeRemoveAction(cmd *cobra.Command, args []string) error {
return err
}
if !force {
- var confirm string
services := "all"
if len(args) != 0 {
services = strings.Join(args, ",")
}
+
msg := fmt.Sprintf("This will remove all stopped containers from services: %s.", services)
- msg += "\nAre you sure you want to continue? [y/N] "
- fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg)
- fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm)
- if strings.ToLower(confirm) != "y" {
- return nil
+ if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed {
+ return err
}
}
diff --git a/cmd/nerdctl/compose_rm_linux_test.go b/cmd/nerdctl/compose/compose_rm_linux_test.go
similarity index 97%
rename from cmd/nerdctl/compose_rm_linux_test.go
rename to cmd/nerdctl/compose/compose_rm_linux_test.go
index a699274883d..948ea9e119d 100644
--- a/cmd/nerdctl/compose_rm_linux_test.go
+++ b/cmd/nerdctl/compose/compose_rm_linux_test.go
@@ -14,14 +14,14 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeRemove(t *testing.T) {
diff --git a/cmd/nerdctl/compose_run.go b/cmd/nerdctl/compose/compose_run.go
similarity index 96%
rename from cmd/nerdctl/compose_run.go
rename to cmd/nerdctl/compose/compose_run.go
index c92c5ab1ae9..afe02895ae0 100644
--- a/cmd/nerdctl/compose_run.go
+++ b/cmd/nerdctl/compose/compose_run.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package compose
import (
"errors"
"fmt"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeRunCommand() *cobra.Command {
@@ -68,7 +70,7 @@ func newComposeRunCommand() *cobra.Command {
}
func composeRunAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go
similarity index 94%
rename from cmd/nerdctl/compose_run_linux_test.go
rename to cmd/nerdctl/compose/compose_run_linux_test.go
index a464e53418a..65b36e7ffb6 100644
--- a/cmd/nerdctl/compose_run_linux_test.go
+++ b/cmd/nerdctl/compose/compose_run_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
@@ -23,11 +23,14 @@ import (
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
)
func TestComposeRun(t *testing.T) {
@@ -94,7 +97,7 @@ services:
stdoutContent := result.Stdout() + result.Stderr()
assert.Assert(psCmd.Base.T, result.ExitCode == 0, stdoutContent)
if strings.Contains(stdoutContent, containerName) {
- logrus.Errorf("test failed, the container %s is not removed", stdoutContent)
+ log.L.Errorf("test failed, the container %s is not removed", stdoutContent)
t.Fail()
return
}
@@ -316,7 +319,7 @@ services:
container := base.InspectContainer(containerName)
if container.Config == nil {
- logrus.Errorf("test failed, cannot fetch container config")
+ log.L.Errorf("test failed, cannot fetch container config")
t.Fail()
}
assert.Equal(t, container.Config.Labels["foo"], "rab")
@@ -424,21 +427,21 @@ func TestComposePushAndPullWithCosignVerify(t *testing.T) {
testutil.RequireExecutable(t, "cosign")
testutil.DockerIncompatible(t)
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
+ t.Parallel()
+
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
+ base.Env = append(base.Env, "COSIGN_PASSWORD=1")
- // set up cosign and local registry
- t.Setenv("COSIGN_PASSWORD", "1")
- keyPair := newCosignKeyPair(t, "cosign-key-pair")
- defer keyPair.cleanup()
+ keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1")
+ reg := testregistry.NewWithNoAuth(base, 0, false)
+ t.Cleanup(func() {
+ keyPair.Cleanup()
+ reg.Cleanup(nil)
+ })
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- localhostIP := "127.0.0.1"
- t.Logf("localhost IP=%q", localhostIP)
- testImageRefPrefix := fmt.Sprintf("%s:%d/",
- localhostIP, reg.ListenPort)
- t.Logf("testImageRefPrefix=%q", testImageRefPrefix)
+ tID := testutil.Identifier(t)
+ testImageRefPrefix := fmt.Sprintf("127.0.0.1:%d/%s/", reg.Port, tID)
var (
imageSvc0 = testImageRefPrefix + "composebuild_svc0"
@@ -473,8 +476,8 @@ services:
x-nerdctl-sign: none
entrypoint:
- stty
-`, imageSvc0, keyPair.publicKey, keyPair.privateKey,
- imageSvc1, keyPair.privateKey, imageSvc2)
+`, imageSvc0, keyPair.PublicKey, keyPair.PrivateKey,
+ imageSvc1, keyPair.PrivateKey, imageSvc2)
dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage)
diff --git a/cmd/nerdctl/compose_start.go b/cmd/nerdctl/compose/compose_start.go
similarity index 88%
rename from cmd/nerdctl/compose_start.go
rename to cmd/nerdctl/compose/compose_start.go
index a5889b18aaa..ce0cde46acc 100644
--- a/cmd/nerdctl/compose_start.go
+++ b/cmd/nerdctl/compose/compose_start.go
@@ -14,21 +14,24 @@
limitations under the License.
*/
-package main
+package compose
import (
"context"
"fmt"
"os"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/labels"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func newComposeStartCommand() *cobra.Command {
@@ -44,7 +47,7 @@ func newComposeStartCommand() *cobra.Command {
}
func composeStartAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go
similarity index 91%
rename from cmd/nerdctl/compose_start_linux_test.go
rename to cmd/nerdctl/compose/compose_start_linux_test.go
index 64567528093..11c1581cd92 100644
--- a/cmd/nerdctl/compose_start_linux_test.go
+++ b/cmd/nerdctl/compose/compose_start_linux_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeStart(t *testing.T) {
@@ -57,10 +57,6 @@ services:
}
func TestComposeStartFailWhenServicePause(t *testing.T) {
- // Incompatible with docker compose v1. Currently CI is using compose v1.
- // Starting a paused container triggers an error in v2 but is ignored in v1.
- testutil.DockerIncompatible(t)
-
base := testutil.NewBase(t)
switch base.Info().CgroupDriver {
case "none", "":
diff --git a/cmd/nerdctl/compose_stop.go b/cmd/nerdctl/compose/compose_stop.go
similarity index 86%
rename from cmd/nerdctl/compose_stop.go
rename to cmd/nerdctl/compose/compose_stop.go
index b35d4911143..2f3382a9822 100644
--- a/cmd/nerdctl/compose_stop.go
+++ b/cmd/nerdctl/compose/compose_stop.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeStopCommand() *cobra.Command {
@@ -36,7 +38,7 @@ func newComposeStopCommand() *cobra.Command {
}
func composeStopAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/compose_stop_linux_test.go b/cmd/nerdctl/compose/compose_stop_linux_test.go
similarity index 81%
rename from cmd/nerdctl/compose_stop_linux_test.go
rename to cmd/nerdctl/compose/compose_stop_linux_test.go
index cadff7f30fd..e10b16ff7b2 100644
--- a/cmd/nerdctl/compose_stop_linux_test.go
+++ b/cmd/nerdctl/compose/compose_stop_linux_test.go
@@ -14,19 +14,16 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeStop(t *testing.T) {
- // docker-compose v2 hides exited/killed containers in `compose ps`, and shows
- // them if `-a` is passed, which is not supported yet by `nerdctl compose`.
- testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
@@ -70,11 +67,11 @@ volumes:
// stop should (only) stop the given service.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")
// `--timeout` arg should work properly.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "--timeout", "5", "wordpress").AssertOK()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Exit", "exited")
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress", "-a").AssertOutContainsAny("Exit", "exited")
}
diff --git a/cmd/nerdctl/compose/compose_test.go b/cmd/nerdctl/compose/compose_test.go
new file mode 100644
index 00000000000..efcfd184e5b
--- /dev/null
+++ b/cmd/nerdctl/compose/compose_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package compose
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/compose_top.go b/cmd/nerdctl/compose/compose_top.go
similarity index 80%
rename from cmd/nerdctl/compose_top.go
rename to cmd/nerdctl/compose/compose_top.go
index 254e1c7a557..0bb8dadccea 100644
--- a/cmd/nerdctl/compose_top.go
+++ b/cmd/nerdctl/compose/compose_top.go
@@ -14,19 +14,22 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/labels"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func newComposeTopCommand() *cobra.Command {
@@ -42,7 +45,7 @@ func newComposeTopCommand() *cobra.Command {
}
func composeTopAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -82,7 +85,7 @@ func composeTopAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
- fmt.Fprintf(stdout, "%s\n", info.Labels[labels.Name])
+ fmt.Fprintln(stdout, info.Labels[labels.Name])
// `compose ps` uses empty ps args
err = container.Top(ctx, client, []string{c.ID()}, types.ContainerTopOptions{
Stdout: cmd.OutOrStdout(),
diff --git a/cmd/nerdctl/compose_top_linux_test.go b/cmd/nerdctl/compose/compose_top_linux_test.go
similarity index 90%
rename from cmd/nerdctl/compose_top_linux_test.go
rename to cmd/nerdctl/compose/compose_top_linux_test.go
index 38a94fa84e9..a0474c51b0b 100644
--- a/cmd/nerdctl/compose_top_linux_test.go
+++ b/cmd/nerdctl/compose/compose_top_linux_test.go
@@ -14,15 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"testing"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeTop(t *testing.T) {
diff --git a/cmd/nerdctl/compose_up.go b/cmd/nerdctl/compose/compose_up.go
similarity index 63%
rename from cmd/nerdctl/compose_up.go
rename to cmd/nerdctl/compose/compose_up.go
index 019bb795d10..0cf2d6bccd2 100644
--- a/cmd/nerdctl/compose_up.go
+++ b/cmd/nerdctl/compose/compose_up.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"errors"
@@ -22,10 +22,12 @@ import (
"strconv"
"strings"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/compose"
- "github.com/containerd/nerdctl/pkg/composer"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/compose"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
)
func newComposeUpCommand() *cobra.Command {
@@ -36,7 +38,8 @@ func newComposeUpCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
- composeUpCommand.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background")
+ composeUpCommand.Flags().Bool("abort-on-container-exit", false, "Stops all containers if any container was stopped. Incompatible with -d.")
+ composeUpCommand.Flags().BoolP("detach", "d", false, "Detached mode: Run containers in the background. Incompatible with --abort-on-container-exit.")
composeUpCommand.Flags().Bool("no-build", false, "Don't build an image, even if it's missing.")
composeUpCommand.Flags().Bool("no-color", false, "Produce monochrome output")
composeUpCommand.Flags().Bool("no-log-prefix", false, "Don't print prefix in logs")
@@ -44,12 +47,15 @@ func newComposeUpCommand() *cobra.Command {
composeUpCommand.Flags().Bool("ipfs", false, "Allow pulling base images from IPFS during build")
composeUpCommand.Flags().Bool("quiet-pull", false, "Pull without printing progress information")
composeUpCommand.Flags().Bool("remove-orphans", false, "Remove containers for services not defined in the Compose file.")
+ composeUpCommand.Flags().Bool("force-recreate", false, "Recreate containers even if their configuration and image haven't changed.")
+ composeUpCommand.Flags().Bool("no-recreate", false, "Don't recreate containers if they exist, conflict with --force-recreate.")
composeUpCommand.Flags().StringArray("scale", []string{}, "Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.")
+ composeUpCommand.Flags().String("pull", "", "Pull image before running (\"always\"|\"missing\"|\"never\")")
return composeUpCommand
}
func composeUpAction(cmd *cobra.Command, services []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -57,6 +63,13 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
if err != nil {
return err
}
+ abortOnContainerExit, err := cmd.Flags().GetBool("abort-on-container-exit")
+ if detach && abortOnContainerExit {
+ return fmt.Errorf("--abort-on-container-exit flag is incompatible with flag --detach")
+ }
+ if err != nil {
+ return err
+ }
noBuild, err := cmd.Flags().GetBool("no-build")
if err != nil {
return err
@@ -84,6 +97,10 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
if err != nil {
return err
}
+ pull, err := cmd.Flags().GetString("pull")
+ if err != nil {
+ return err
+ }
removeOrphans, err := cmd.Flags().GetBool("remove-orphans")
if err != nil {
return err
@@ -92,7 +109,18 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
if err != nil {
return err
}
- scale := make(map[string]uint64)
+ forceRecreate, err := cmd.Flags().GetBool("force-recreate")
+ if err != nil {
+ return err
+ }
+ noRecreate, err := cmd.Flags().GetBool("no-recreate")
+ if err != nil {
+ return err
+ }
+ if forceRecreate && noRecreate {
+ return errors.New("flag --force-recreate and --no-recreate cannot be specified together")
+ }
+ scale := make(map[string]int)
for _, s := range scaleSlice {
parts := strings.Split(s, "=")
if len(parts) != 2 {
@@ -102,7 +130,7 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
if err != nil {
return err
}
- scale[parts[0]] = uint64(replicas)
+ scale[parts[0]] = replicas
}
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
@@ -121,15 +149,19 @@ func composeUpAction(cmd *cobra.Command, services []string) error {
}
uo := composer.UpOptions{
- Detach: detach,
- NoBuild: noBuild,
- NoColor: noColor,
- NoLogPrefix: noLogPrefix,
- ForceBuild: build,
- IPFS: enableIPFS,
- QuietPull: quietPull,
- RemoveOrphans: removeOrphans,
- Scale: scale,
+ AbortOnContainerExit: abortOnContainerExit,
+ Detach: detach,
+ NoBuild: noBuild,
+ NoColor: noColor,
+ NoLogPrefix: noLogPrefix,
+ ForceBuild: build,
+ IPFS: enableIPFS,
+ QuietPull: quietPull,
+ RemoveOrphans: removeOrphans,
+ Scale: scale,
+ Pull: pull,
+ ForceRecreate: forceRecreate,
+ NoRecreate: noRecreate,
}
return c.Up(ctx, uo, services)
}
diff --git a/cmd/nerdctl/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go
similarity index 79%
rename from cmd/nerdctl/compose_up_linux_test.go
rename to cmd/nerdctl/compose/compose_up_linux_test.go
index c6b5c34773a..6da3162b4b9 100644
--- a/cmd/nerdctl/compose_up_linux_test.go
+++ b/cmd/nerdctl/compose/compose_up_linux_test.go
@@ -14,30 +14,31 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"io"
- "os"
"strings"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
"github.com/docker/go-connections/nat"
- "github.com/sirupsen/logrus"
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/icmd"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
+ "github.com/containerd/log"
- "gotest.tools/v3/assert"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
)
func TestComposeUp(t *testing.T) {
base := testutil.NewBase(t)
- testComposeUp(t, base, fmt.Sprintf(`
+ helpers.ComposeUp(t, base, fmt.Sprintf(`
version: '3.1'
services:
@@ -72,61 +73,10 @@ volumes:
`, testutil.WordpressImage, testutil.MariaDBImage))
}
-func testComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) {
- comp := testutil.NewComposeDir(t, dockerComposeYAML)
- defer comp.CleanUp()
-
- projectName := comp.ProjectName()
- t.Logf("projectName=%q", projectName)
-
- base.ComposeCmd(append(append([]string{"-f", comp.YAMLFullPath()}, opts...), "up", "-d")...).AssertOK()
- defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
- base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertOK()
- base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK()
-
- checkWordpress := func() error {
- resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false)
- if err != nil {
- return err
- }
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- return err
- }
- if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) {
- t.Logf("respBody=%q", respBody)
- return fmt.Errorf("respBody does not contain %q", testutil.WordpressIndexHTMLSnippet)
- }
- return nil
- }
-
- var wordpressWorking bool
- for i := 0; i < 30; i++ {
- t.Logf("(retry %d)", i)
- err := checkWordpress()
- if err == nil {
- wordpressWorking = true
- break
- }
- // NOTE: "Error establishing a database connection
" is expected for the first few iterations
- t.Log(err)
- time.Sleep(3 * time.Second)
- }
-
- if !wordpressWorking {
- t.Fatal("wordpress is not working")
- }
- t.Log("wordpress seems functional")
-
- base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
- base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail()
- base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail()
-}
-
func TestComposeUpBuild(t *testing.T) {
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
const dockerComposeYAML = `
services:
@@ -194,7 +144,7 @@ networks:
stdoutContent := result.Stdout() + result.Stderr()
assert.Assert(inspectCmd.Base.T, result.ExitCode == 0, stdoutContent)
if !strings.Contains(stdoutContent, staticIP) {
- logrus.Errorf("test failed, the actual container ip is %s", stdoutContent)
+ log.L.Errorf("test failed, the actual container ip is %s", stdoutContent)
t.Fail()
return
}
@@ -268,7 +218,7 @@ services:
projectName := comp.ProjectName()
t.Logf("projectName=%q", projectName)
- base.Env = append(os.Environ(), "ADDRESS=0.0.0.0")
+ base.Env = append(base.Env, "ADDRESS=0.0.0.0")
base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
@@ -505,7 +455,7 @@ func TestComposeUpWithBypass4netns(t *testing.T) {
testutil.RequireKernelVersion(t, ">= 5.9.0-0")
testutil.RequireSystemService(t, "bypass4netnsd")
base := testutil.NewBase(t)
- testComposeUp(t, base, fmt.Sprintf(`
+ helpers.ComposeUp(t, base, fmt.Sprintf(`
version: '3.1'
services:
@@ -522,7 +472,7 @@ services:
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
- labels:
+ annotations:
- nerdctl/bypass4netns=1
db:
@@ -535,7 +485,7 @@ services:
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
- labels:
+ annotations:
- nerdctl/bypass4netns=1
volumes:
@@ -585,3 +535,101 @@ services:
psCmd.AssertOutContains(serviceRegular)
psCmd.AssertOutNotContains(serviceProfiled)
}
+
+func TestComposeUpAbortOnContainerExit(t *testing.T) {
+ base := testutil.NewBase(t)
+ serviceRegular := "regular"
+ serviceProfiled := "exited"
+ dockerComposeYAML := fmt.Sprintf(`
+services:
+ %s:
+ image: %s
+ ports:
+ - 8080:80
+ %s:
+ image: %s
+ entrypoint: /bin/sh -c "exit 1"
+`, serviceRegular, testutil.NginxAlpineImage, serviceProfiled, testutil.BusyboxImage)
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ // here we run 'compose up --abort-on-container-exit' command
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--abort-on-container-exit").AssertExitCode(1)
+ time.Sleep(3 * time.Second)
+ psCmd := base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited")
+
+ psCmd.AssertOutContains(serviceRegular)
+ psCmd.AssertOutContains(serviceProfiled)
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
+
+ // this time we run 'compose up' command without --abort-on-container-exit flag
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
+ time.Sleep(3 * time.Second)
+ psCmd = base.Cmd("ps", "-a", "--format={{.Names}}", "--filter", "status=exited")
+
+ // this time the regular service should not be listed in the output
+ psCmd.AssertOutNotContains(serviceRegular)
+ psCmd.AssertOutContains(serviceProfiled)
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
+
+ // in this sub-test we are ensuring that flags '-d' and '--abort-on-container-exit' cannot be ran together
+ c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--abort-on-container-exit")
+ expected := icmd.Expected{
+ ExitCode: 1,
+ }
+ c.Assert(expected)
+}
+
+func TestComposeUpPull(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ var dockerComposeYAML = fmt.Sprintf(`
+services:
+ test:
+ image: %s
+ command: sh -euxc "echo hi"
+`, testutil.CommonImage)
+
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ // Cases where pull is required
+ for _, pull := range []string{"missing", "always"} {
+ t.Run(fmt.Sprintf("pull=%s", pull), func(t *testing.T) {
+ base.Cmd("rmi", "-f", testutil.CommonImage).Run()
+ base.Cmd("images").AssertOutNotContains(testutil.CommonImage)
+ t.Cleanup(func() {
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK()
+ })
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", pull).AssertOutContains("hi")
+ })
+ }
+
+ t.Run("pull=never, no pull", func(t *testing.T) {
+ base.Cmd("rmi", "-f", testutil.CommonImage).Run()
+ base.Cmd("images").AssertOutNotContains(testutil.CommonImage)
+ t.Cleanup(func() {
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK()
+ })
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "--pull", "never").AssertExitCode(1)
+ })
+}
+
+func TestComposeUpServicePullPolicy(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ var dockerComposeYAML = fmt.Sprintf(`
+services:
+ test:
+ image: %s
+ command: sh -euxc "echo hi"
+ pull_policy: "never"
+`, testutil.CommonImage)
+
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ base.Cmd("rmi", "-f", testutil.CommonImage).Run()
+ base.Cmd("images").AssertOutNotContains(testutil.CommonImage)
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertExitCode(1)
+}
diff --git a/cmd/nerdctl/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go
similarity index 94%
rename from cmd/nerdctl/compose_up_test.go
rename to cmd/nerdctl/compose/compose_up_test.go
index ef1811fcd8e..63bba829fcf 100644
--- a/cmd/nerdctl/compose_up_test.go
+++ b/cmd/nerdctl/compose/compose_up_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
@@ -23,9 +23,10 @@ import (
"runtime"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
// https://github.com/containerd/nerdctl/issues/1942
@@ -49,7 +50,7 @@ services:
Err: `exec: \"invalid\": executable file not found in $PATH`,
}
if base.Target == testutil.Docker {
- expected.Err = `Unknown runtime specified invalid`
+ expected.Err = `unknown or invalid runtime name: invalid`
}
c.Assert(expected)
}
diff --git a/cmd/nerdctl/compose_version.go b/cmd/nerdctl/compose/compose_version.go
similarity index 92%
rename from cmd/nerdctl/compose_version.go
rename to cmd/nerdctl/compose/compose_version.go
index dbea1318ec6..d4a74a4bca0 100644
--- a/cmd/nerdctl/compose_version.go
+++ b/cmd/nerdctl/compose/compose_version.go
@@ -14,14 +14,15 @@
limitations under the License.
*/
-package main
+package compose
import (
"fmt"
"strings"
- "github.com/containerd/nerdctl/pkg/version"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/pkg/version"
)
func newComposeVersionCommand() *cobra.Command {
@@ -47,7 +48,7 @@ func composeVersionAction(cmd *cobra.Command, args []string) error {
return err
}
if short {
- fmt.Fprintln(cmd.OutOrStdout(), strings.TrimPrefix(version.Version, "v"))
+ fmt.Fprintln(cmd.OutOrStdout(), strings.TrimPrefix(version.GetVersion(), "v"))
return nil
}
@@ -57,7 +58,7 @@ func composeVersionAction(cmd *cobra.Command, args []string) error {
}
switch format {
case "pretty":
- fmt.Fprintln(cmd.OutOrStdout(), "nerdctl Compose version "+version.Version)
+ fmt.Fprintln(cmd.OutOrStdout(), "nerdctl Compose version "+version.GetVersion())
case "json":
fmt.Fprintf(cmd.OutOrStdout(), "{\"version\":\"%v\"}\n", version.Version)
default:
diff --git a/cmd/nerdctl/compose_version_test.go b/cmd/nerdctl/compose/compose_version_test.go
similarity index 94%
rename from cmd/nerdctl/compose_version_test.go
rename to cmd/nerdctl/compose/compose_version_test.go
index 2f3354745f3..af3028b3d65 100644
--- a/cmd/nerdctl/compose_version_test.go
+++ b/cmd/nerdctl/compose/compose_version_test.go
@@ -14,12 +14,12 @@
limitations under the License.
*/
-package main
+package compose
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestComposeVersion(t *testing.T) {
diff --git a/cmd/nerdctl/container.go b/cmd/nerdctl/container/container.go
similarity index 59%
rename from cmd/nerdctl/container.go
rename to cmd/nerdctl/container/container.go
index 6c0a763f14b..21c7f9b63b1 100644
--- a/cmd/nerdctl/container.go
+++ b/cmd/nerdctl/container/container.go
@@ -14,48 +14,53 @@
limitations under the License.
*/
-package main
+package container
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newContainerCommand() *cobra.Command {
+func NewContainerCommand() *cobra.Command {
containerCommand := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "container",
Short: "Manage containers",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
containerCommand.AddCommand(
- newCreateCommand(),
- newRunCommand(),
- newUpdateCommand(),
- newExecCommand(),
+ NewCreateCommand(),
+ NewRunCommand(),
+ NewUpdateCommand(),
+ NewExecCommand(),
containerLsCommand(),
newContainerInspectCommand(),
- newLogsCommand(),
- newPortCommand(),
- newRmCommand(),
- newStopCommand(),
- newStartCommand(),
- newRestartCommand(),
- newKillCommand(),
- newPauseCommand(),
- newWaitCommand(),
- newUnpauseCommand(),
- newCommitCommand(),
- newRenameCommand(),
+ NewLogsCommand(),
+ NewPortCommand(),
+ NewRmCommand(),
+ NewStopCommand(),
+ NewStartCommand(),
+ NewRestartCommand(),
+ NewKillCommand(),
+ NewPauseCommand(),
+ NewDiffCommand(),
+ NewWaitCommand(),
+ NewUnpauseCommand(),
+ NewCommitCommand(),
+ NewRenameCommand(),
newContainerPruneCommand(),
+ NewStatsCommand(),
+ NewAttachCommand(),
)
- addCpCommand(containerCommand)
+ AddCpCommand(containerCommand)
return containerCommand
}
func containerLsCommand() *cobra.Command {
- x := newPsCommand()
+ x := NewPsCommand()
x.Use = "ls"
x.Aliases = []string{"list"}
return x
diff --git a/cmd/nerdctl/container/container_attach.go b/cmd/nerdctl/container/container_attach.go
new file mode 100644
index 00000000000..e92aa44b26d
--- /dev/null
+++ b/cmd/nerdctl/container/container_attach.go
@@ -0,0 +1,100 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
+)
+
+func NewAttachCommand() *cobra.Command {
+ const shortHelp = "Attach stdin, stdout, and stderr to a running container."
+ const longHelp = `Attach stdin, stdout, and stderr to a running container. For example:
+
+1. 'nerdctl run -it --name test busybox' to start a container with a pty
+2. 'ctrl-p ctrl-q' to detach from the container
+3. 'nerdctl attach test' to attach to the container
+
+Caveats:
+
+- Currently only one attach session is allowed. When the second session tries to attach, currently no error will be returned from nerdctl.
+ However, since behind the scenes, there's only one FIFO for stdin, stdout, and stderr respectively,
+ if there are multiple sessions, all the sessions will be reading from and writing to the same 3 FIFOs, which will result in mixed input and partial output.
+- Until dual logging (issue #1946) is implemented,
+ a container that is spun up by either 'nerdctl run -d' or 'nerdctl start' (without '--attach') cannot be attached to.`
+
+ var attachCommand = &cobra.Command{
+ Use: "attach [flags] CONTAINER",
+ Args: cobra.ExactArgs(1),
+ Short: shortHelp,
+ Long: longHelp,
+ RunE: containerAttachAction,
+ ValidArgsFunction: attachShellComplete,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+ attachCommand.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys")
+ return attachCommand
+}
+
+func processContainerAttachOptions(cmd *cobra.Command) (types.ContainerAttachOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.ContainerAttachOptions{}, err
+ }
+ detachKeys, err := cmd.Flags().GetString("detach-keys")
+ if err != nil {
+ return types.ContainerAttachOptions{}, err
+ }
+ return types.ContainerAttachOptions{
+ GOptions: globalOptions,
+ Stdin: cmd.InOrStdin(),
+ Stdout: cmd.OutOrStdout(),
+ Stderr: cmd.ErrOrStderr(),
+ DetachKeys: detachKeys,
+ }, nil
+}
+
+func containerAttachAction(cmd *cobra.Command, args []string) error {
+ options, err := processContainerAttachOptions(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
+
+ return container.Attach(ctx, client, args[0], options)
+}
+
+func attachShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ statusFilterFn := func(st containerd.ProcessStatus) bool {
+ return st == containerd.Running
+ }
+ return completion.ContainerNames(cmd, statusFilterFn)
+}
diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go
new file mode 100644
index 00000000000..8d90897230e
--- /dev/null
+++ b/cmd/nerdctl/container/container_attach_linux_test.go
@@ -0,0 +1,161 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+// skipAttachForDocker should be called by attach-related tests that assert 'read detach keys' in stdout.
+func skipAttachForDocker(t *testing.T) {
+ t.Helper()
+ if testutil.GetTarget() == testutil.Docker {
+ t.Skip("When detaching from a container, for a session started with 'docker attach'" +
+ ", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
+ " However, the flag is called '--detach-keys' in all cases" +
+ ", so nerdctl prints 'read detach keys' for all cases" +
+ ", and that's why this test is skipped for Docker.")
+ }
+}
+
+// prepareContainerToAttach spins up a container (entrypoint = shell) with `-it` and detaches from it
+// so that it can be re-attached to later.
+func prepareContainerToAttach(base *testutil.Base, containerName string) {
+ opts := []func(*testutil.Cmd){
+ testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
+ []byte{16, 17}, // ctrl+p,ctrl+q, see https://www.physics.udel.edu/~watson/scen103/ascii.html
+ ))),
+ }
+ // unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
+ // unbuffer(1) can be installed with `apt-get install expect`.
+ //
+ // "-p" is needed because we need unbuffer to read from stdin, and from [1]:
+ // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
+ // To use unbuffer in a pipeline, use the -p flag."
+ //
+ // [1] https://linux.die.net/man/1/unbuffer
+ base.CmdWithHelper([]string{"unbuffer", "-p"}, "run", "-it", "--name", containerName, testutil.CommonImage).
+ CmdOption(opts...).AssertOutContains("read detach keys")
+ container := base.InspectContainer(containerName)
+ assert.Equal(base.T, container.State.Running, true)
+}
+
+func TestAttach(t *testing.T) {
+ t.Parallel()
+
+ t.Skip("This test is very unstable and currently skipped. See https://github.com/containerd/nerdctl/issues/3558")
+
+ skipAttachForDocker(t)
+
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+
+ defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
+ prepareContainerToAttach(base, containerName)
+
+ opts := []func(*testutil.Cmd){
+ testutil.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("expr 1 + 1\nexit\n"))),
+ }
+ // `unbuffer -p` returns 0 even if the underlying nerdctl process returns a non-zero exit code,
+ // so the exit code cannot be easily tested here.
+ base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", containerName).CmdOption(opts...).AssertOutContains("2")
+ container := base.InspectContainer(containerName)
+ assert.Equal(base.T, container.State.Running, false)
+}
+
+func TestAttachDetachKeys(t *testing.T) {
+ t.Parallel()
+
+ skipAttachForDocker(t)
+
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+
+ defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
+ prepareContainerToAttach(base, containerName)
+
+ opts := []func(*testutil.Cmd){
+ testutil.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader(
+ []byte{1, 2}, // https://www.physics.udel.edu/~watson/scen103/ascii.html
+ ))),
+ }
+ base.CmdWithHelper([]string{"unbuffer", "-p"}, "attach", "--detach-keys=ctrl-a,ctrl-b", containerName).
+ CmdOption(opts...).AssertOutContains("read detach keys")
+ container := base.InspectContainer(containerName)
+ assert.Equal(base.T, container.State.Running, true)
+}
+
+// TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568
+func TestDetachAttachKeysForAutoRemovedContainer(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Issue #3568 - A container should be deleted when detaching and attaching a container started with the --rm option.",
+ // In nerdctl the detach return code from the container is 0, but in docker the return code is 1.
+ // This behaviour is reported in https://github.com/containerd/nerdctl/issues/3571 so this test is skipped for Docker.
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage)
+ // unbuffer(1) can be installed with `apt-get install expect`.
+ //
+ // "-p" is needed because we need unbuffer to read from stdin, and from [1]:
+ // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
+ // To use unbuffer in a pipeline, use the -p flag."
+ //
+ // [1] https://linux.die.net/man/1/unbuffer
+ cmd.WithWrapper("unbuffer", "-p")
+ cmd.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))) // https://www.physics.udel.edu/~watson/scen103/ascii.html
+ cmd.Run(&test.Expected{
+ ExitCode: 0,
+ })
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("attach", data.Identifier())
+ cmd.WithWrapper("unbuffer", "-p")
+ cmd.WithStdin(testutil.NewDelayOnceReader(strings.NewReader("exit\n")))
+ return cmd
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: test.All(
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, !strings.Contains(helpers.Capture("ps", "-a"), data.Identifier()))
+ },
+ ),
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_commit.go b/cmd/nerdctl/container/container_commit.go
similarity index 85%
rename from cmd/nerdctl/container_commit.go
rename to cmd/nerdctl/container/container_commit.go
index ad09be63e37..e7276499ab3 100644
--- a/cmd/nerdctl/container_commit.go
+++ b/cmd/nerdctl/container/container_commit.go
@@ -14,20 +14,23 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newCommitCommand() *cobra.Command {
+func NewCommitCommand() *cobra.Command {
var commitCommand = &cobra.Command{
Use: "commit [flags] CONTAINER REPOSITORY[:TAG]",
Short: "Create a new image from a container's changes",
- Args: IsExactArgs(2),
+ Args: helpers.IsExactArgs(2),
RunE: commitAction,
ValidArgsFunction: commitShellComplete,
SilenceUsage: true,
@@ -41,7 +44,7 @@ func newCommitCommand() *cobra.Command {
}
func processCommitCommandOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerCommitOptions{}, err
}
@@ -91,7 +94,7 @@ func commitAction(cmd *cobra.Command, args []string) error {
func commitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go
new file mode 100644
index 00000000000..ca9bb15f9fd
--- /dev/null
+++ b/cmd/nerdctl/container/container_commit_linux_test.go
@@ -0,0 +1,91 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestKubeCommitSave(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.OnlyKubernetes
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ containerID := ""
+ // NOTE: kubectl namespaces are not the same as containerd namespaces.
+ // We still want kube test objects segregated in their own Kube API namespace.
+ nerdtest.KubeCtlCommand(helpers, "create", "namespace", "nerdctl-test-k8s").Run(&test.Expected{})
+ nerdtest.KubeCtlCommand(helpers, "run", "--image", testutil.CommonImage, identifier, "--", "sleep", nerdtest.Infinity).Run(&test.Expected{})
+ nerdtest.KubeCtlCommand(helpers, "wait", "pod", identifier, "--for=condition=ready", "--timeout=1m").Run(&test.Expected{})
+ nerdtest.KubeCtlCommand(helpers, "exec", identifier, "--", "mkdir", "-p", "/tmp/whatever").Run(&test.Expected{})
+ nerdtest.KubeCtlCommand(helpers, "get", "pods", identifier, "-o", "jsonpath={ .status.containerStatuses[0].containerID }").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ containerID = strings.TrimPrefix(stdout, "containerd://")
+ },
+ })
+ data.Set("containerID", containerID)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ nerdtest.KubeCtlCommand(helpers, "delete", "pod", "--all").Run(nil)
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ helpers.Ensure("commit", data.Get("containerID"), "testcommitsave")
+ return helpers.Command("save", "testcommitsave")
+ }
+
+ testCase.Expected = test.Expects(0, nil, nil)
+
+ testCase.Run(t)
+
+ // This below is missing configuration to allow for plain http communication
+ // This is left here for future work to successfully start a registry usable in the cluster
+ /*
+ // Start a registry
+ nerdtest.KubeCtlCommand(helpers, "run", "--port", "5000", "--image", testutil.RegistryImageStable, "testregistry").
+ Run(&test.Expected{})
+
+ nerdtest.KubeCtlCommand(helpers, "wait", "pod", "testregistry", "--for=condition=ready", "--timeout=1m").
+ AssertOK()
+
+ cmd = nerdtest.KubeCtlCommand(helpers, "get", "pods", tID, "-o", "jsonpath={ .status.hostIPs[0].ip }")
+ cmd.Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ registryIP = stdout
+ },
+ })
+
+ cmd = nerdtest.KubeCtlCommand(helpers, "apply", "-f", "-", fmt.Sprintf(`apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ name: local-registry
+ namespace: nerdctl-test
+ data:
+ localRegistryHosting.v1: |
+ host: "%s:5000"
+ help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
+ `, registryIP))
+ */
+}
diff --git a/cmd/nerdctl/container/container_commit_test.go b/cmd/nerdctl/container/container_commit_test.go
new file mode 100644
index 00000000000..22d3a3bf02c
--- /dev/null
+++ b/cmd/nerdctl/container/container_commit_test.go
@@ -0,0 +1,85 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestCommit(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "with pause",
+ Require: nerdtest.CGroup,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("rm", "-f", identifier)
+ helpers.Anyhow("rmi", "-f", identifier)
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ identifier := data.Identifier()
+ helpers.Ensure(
+ "commit",
+ "-c", `CMD ["/foo"]`,
+ "-c", `ENTRYPOINT ["cat"]`,
+ "--pause=true",
+ identifier, identifier)
+ return helpers.Command("run", "--rm", identifier)
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello-test-commit\n")),
+ },
+ {
+ Description: "no pause",
+ Require: test.Not(test.Windows),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("rm", "-f", identifier)
+ helpers.Anyhow("rmi", "-f", identifier)
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
+ nerdtest.EnsureContainerStarted(helpers, identifier)
+ helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ identifier := data.Identifier()
+ helpers.Ensure(
+ "commit",
+ "-c", `CMD ["/foo"]`,
+ "-c", `ENTRYPOINT ["cat"]`,
+ "--pause=false",
+ identifier, identifier)
+ return helpers.Command("run", "--rm", identifier)
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello-test-commit\n")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container/container_cp_acid_linux_test.go b/cmd/nerdctl/container/container_cp_acid_linux_test.go
new file mode 100644
index 00000000000..fc30c4ab314
--- /dev/null
+++ b/cmd/nerdctl/container/container_cp_acid_linux_test.go
@@ -0,0 +1,181 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/icmd"
+
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+// This is a separate set of tests for cp specifically meant to test corner or extreme cases that do not fit in the normal testing rig
+// because of their complexity
+
+func TestCopyAcid(t *testing.T) {
+ t.Parallel()
+
+ t.Run("Travelling along volumes w/o read-only", func(t *testing.T) {
+ t.Parallel()
+ testID := testutil.Identifier(t)
+ tempDir := t.TempDir()
+ base := testutil.NewBase(t)
+ base.Dir = tempDir
+
+ sourceFile := filepath.Join(tempDir, "hostfile")
+ sourceFileContent := []byte(testID)
+
+ roContainer := testID + "-ro"
+ rwContainer := testID + "-rw"
+
+ setup := func() {
+ base.Cmd("volume", "create", testID+"-1-ro").AssertOK()
+ base.Cmd("volume", "create", testID+"-2-rw").AssertOK()
+ base.Cmd("volume", "create", testID+"-3-rw").AssertOK()
+ base.Cmd("run", "-d", "-w", containerCwd, "--name", roContainer, "--read-only",
+ "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"),
+ "-v", fmt.Sprintf("%s:%s", testID+"-2-rw", "/vol2/dir2/rw"),
+ testutil.CommonImage, "sleep", "Inf",
+ ).AssertOK()
+ base.Cmd("run", "-d", "-w", containerCwd, "--name", rwContainer,
+ "-v", fmt.Sprintf("%s:%s:ro", testID+"-1-ro", "/vol1/dir1/ro"),
+ "-v", fmt.Sprintf("%s:%s", testID+"-3-rw", "/vol3/dir3/rw"),
+ testutil.CommonImage, "sleep", "Inf",
+ ).AssertOK()
+
+ base.Cmd("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s ../../../ relativelinktoroot").AssertOK()
+ base.Cmd("exec", rwContainer, "sh", "-euxc", "cd /vol3/dir3/rw; ln -s / absolutelinktoroot").AssertOK()
+ base.Cmd("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s ../../../ relativelinktoroot").AssertOK()
+ base.Cmd("exec", roContainer, "sh", "-euxc", "cd /vol2/dir2/rw; ln -s / absolutelinktoroot").AssertOK()
+ // Create file on the host
+ err := os.WriteFile(sourceFile, sourceFileContent, filePerm)
+ assert.NilError(t, err)
+ }
+
+ tearDown := func() {
+ base.Cmd("rm", "-f", roContainer).Run()
+ base.Cmd("rm", "-f", rwContainer).Run()
+ base.Cmd("volume", "rm", testID+"-1-ro").Run()
+ base.Cmd("volume", "rm", testID+"-2-rw").Run()
+ base.Cmd("volume", "rm", testID+"-3-rw").Run()
+ }
+
+ t.Cleanup(tearDown)
+ tearDown()
+
+ setup()
+
+ expectedErr := containerutil.ErrTargetIsReadOnly.Error()
+ if testutil.GetTarget() == testutil.Docker {
+ expectedErr = ""
+ }
+
+ t.Run("Cannot copy into a read-only root", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, roContainer+":/").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Cannot copy into a read-only mount, in a rw container", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, rwContainer+":/vol1/dir1/ro").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Can copy into a read-write mount in a read-only container", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw").Assert(icmd.Expected{
+ ExitCode: 0,
+ })
+ })
+
+ t.Run("Traverse read-only locations to a read-write location", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, roContainer+":/vol1/dir1/ro/../../../vol2/dir2/rw").Assert(icmd.Expected{
+ ExitCode: 0,
+ })
+ })
+
+ t.Run("Follow an absolute symlink inside a read-write mount to a read-only root", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw/absolutelinktoroot").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Follow am absolute symlink inside a read-write mount to a read-only mount", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, rwContainer+":/vol3/dir3/rw/absolutelinktoroot/vol1/dir1/ro").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Follow a relative symlink inside a read-write location to a read-only root", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, roContainer+":/vol2/dir2/rw/relativelinktoroot").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Follow a relative symlink inside a read-write location to a read-only mount", func(t *testing.T) {
+ t.Parallel()
+
+ base.Cmd("cp", sourceFile, rwContainer+":/vol3/dir3/rw/relativelinktoroot/vol1/dir1/ro").Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ t.Run("Cannot copy into a HOST read-only location", func(t *testing.T) {
+ t.Parallel()
+
+ // Root will just ignore the 000 permission on the host directory.
+ if !rootlessutil.IsRootless() {
+ t.Skip("This test does not work rootful")
+ }
+
+ err := os.MkdirAll(filepath.Join(tempDir, "rotest"), 0o000)
+ assert.NilError(t, err)
+ base.Cmd("cp", roContainer+":/etc/issue", filepath.Join(tempDir, "rotest")).Assert(icmd.Expected{
+ ExitCode: 1,
+ Err: expectedErr,
+ })
+ })
+
+ })
+}
diff --git a/pkg/cmd/container/run_cgroup_windows.go b/cmd/nerdctl/container/container_cp_freebsd.go
similarity index 73%
rename from pkg/cmd/container/run_cgroup_windows.go
rename to cmd/nerdctl/container/container_cp_freebsd.go
index 7ae22379ef9..4e7d2cfd518 100644
--- a/pkg/cmd/container/run_cgroup_windows.go
+++ b/cmd/nerdctl/container/container_cp_freebsd.go
@@ -16,11 +16,8 @@
package container
-import (
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
-)
+import "github.com/spf13/cobra"
-func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.SpecOpts, error) {
- return []oci.SpecOpts{}, nil
+func AddCpCommand(rootCmd *cobra.Command) {
+ // NOP
}
diff --git a/cmd/nerdctl/container_cp_linux.go b/cmd/nerdctl/container/container_cp_linux.go
similarity index 61%
rename from cmd/nerdctl/container_cp_linux.go
rename to cmd/nerdctl/container/container_cp_linux.go
index dc412ad3257..e9af6a7282d 100644
--- a/cmd/nerdctl/container_cp_linux.go
+++ b/cmd/nerdctl/container/container_cp_linux.go
@@ -14,23 +14,24 @@
limitations under the License.
*/
-package main
+package container
import (
- "encoding/json"
+ "errors"
"fmt"
- "os"
- "os/exec"
+ "path/filepath"
+ "strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
func newCpCommand() *cobra.Command {
-
shortHelp := "Copy files/folders between a running container and the local filesystem."
longHelp := shortHelp + `
@@ -46,7 +47,7 @@ Using 'nerdctl cp' with untrusted or malicious containers is unsupported and may
nerdctl cp [flags] SRC_PATH|- CONTAINER:DEST_PATH`
var cpCommand = &cobra.Command{
Use: usage,
- Args: IsExactArgs(2),
+ Args: helpers.IsExactArgs(2),
Short: shortHelp,
Long: longHelp,
RunE: cpAction,
@@ -65,11 +66,26 @@ func cpAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
+ if rootlessutil.IsRootless() {
+ options.GOptions.Address, err = rootlessutil.RootlessContainredSockAddress()
+ if err != nil {
+ return err
+ }
+ }
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
- return container.Cp(cmd.Context(), options)
+ return container.Cp(ctx, client, options)
}
func processCpOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.ContainerCpOptions{}, err
+ }
flagL, err := cmd.Flags().GetBool("follow-link")
if err != nil {
return types.ContainerCpOptions{}, err
@@ -99,40 +115,65 @@ func processCpOptions(cmd *cobra.Command, args []string) (types.ContainerCpOptio
}
container2host := srcSpec.Container != nil
- var container string
+ var containerReq string
if container2host {
- container = *srcSpec.Container
+ containerReq = *srcSpec.Container
} else {
- container = *destSpec.Container
+ containerReq = *destSpec.Container
}
- ctx := cmd.Context()
+ return types.ContainerCpOptions{
+ GOptions: globalOptions,
+ Container2Host: container2host,
+ ContainerReq: containerReq,
+ DestPath: destSpec.Path,
+ SrcPath: srcSpec.Path,
+ FollowSymLink: flagL,
+ }, nil
+}
+
+func AddCpCommand(rootCmd *cobra.Command) {
+ rootCmd.AddCommand(newCpCommand())
+}
- // cp works in the host namespace (for inspecting file permissions), so we can't directly use the Go client.
+var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path")
- selfExe, inspectArgs := globalFlags(cmd)
- inspectArgs = append(inspectArgs, "container", "inspect", "--mode=native", "--format={{json .Process}}", container)
- inspectCmd := exec.CommandContext(ctx, selfExe, inspectArgs...)
- inspectCmd.Stderr = os.Stderr
- inspectOut, err := inspectCmd.Output()
- if err != nil {
- return types.ContainerCpOptions{}, fmt.Errorf("failed to execute %v: %w", inspectCmd.Args, err)
- }
- var proc native.Process
- if err := json.Unmarshal(inspectOut, &proc); err != nil {
- return types.ContainerCpOptions{}, err
+func parseCpFileSpec(arg string) (*cpFileSpec, error) {
+ i := strings.Index(arg, ":")
+
+ // filespec starting with a semicolon is invalid
+ if i == 0 {
+ return nil, errFileSpecDoesntMatchFormat
}
- if proc.Status.Status != containerd.Running {
- return types.ContainerCpOptions{}, fmt.Errorf("expected container status %v, got %v", containerd.Running, proc.Status.Status)
+
+ if filepath.IsAbs(arg) {
+ // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
+ return &cpFileSpec{
+ Container: nil,
+ Path: arg,
+ }, nil
}
- if proc.Pid <= 0 {
- return types.ContainerCpOptions{}, fmt.Errorf("got non-positive PID %v", proc.Pid)
+
+ parts := strings.SplitN(arg, ":", 2)
+
+ if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
+ // Either there's no `:` in the arg
+ // OR it's an explicit local relative path like `./file:name.txt`.
+ return &cpFileSpec{
+ Path: arg,
+ }, nil
}
- return types.ContainerCpOptions{
- Container2Host: container2host,
- Pid: proc.Pid,
- DestPath: destSpec.Path,
- SrcPath: srcSpec.Path,
- FollowSymLink: flagL,
+ return &cpFileSpec{
+ Container: &parts[0],
+ Path: parts[1],
}, nil
}
+
+type cpFileSpec struct {
+ Container *string
+ Path string
+}
+
+func cpShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return nil, cobra.ShellCompDirectiveFilterFileExt
+}
diff --git a/cmd/nerdctl/container/container_cp_linux_test.go b/cmd/nerdctl/container/container_cp_linux_test.go
new file mode 100644
index 00000000000..a584b722243
--- /dev/null
+++ b/cmd/nerdctl/container/container_cp_linux_test.go
@@ -0,0 +1,957 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+
+ "gotest.tools/v3/assert"
+ "gotest.tools/v3/icmd"
+
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+// For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/
+// Obviously, none of this is fully windows ready - obviously `nerdctl cp` itself is not either, so, ok for now.
+const (
+ // Use this to poke the testing rig for improper path handling
+ // TODO: fuzz this more seriously
+ // FIXME: the following will break the test (anything that will evaluate on the shell, obviously):
+ // - `
+ // - $a, ${a}, etc
+ complexify = "" // = "-~a0-_.(){}[]*#! \"'∞"
+
+ pathDoesNotExistRelative = "does-not-exist" + complexify
+ pathDoesNotExistAbsolute = string(os.PathSeparator) + "does-not-exist" + complexify
+ pathIsAFileRelative = "is-a-file" + complexify
+ pathIsAFileAbsolute = string(os.PathSeparator) + "is-a-file" + complexify
+ pathIsADirRelative = "is-a-dir" + complexify
+ pathIsADirAbsolute = string(os.PathSeparator) + "is-a-dir" + complexify
+ pathIsAVolumeMount = string(os.PathSeparator) + "is-a-volume-mount" + complexify
+
+ srcFileName = "test-file" + complexify
+
+ // Since nerdctl cp must NOT obey container wd, but instead resolve paths against the root, we set this
+ // explicitly to ensure we do the right thing wrt that.
+ containerCwd = "/nerdctl/cp/test"
+
+ dirPerm = 0o755
+ filePerm = 0o644
+)
+
+var srcDirName = filepath.Join("three-levels-src-dir", "test-dir", "dir"+complexify)
+
+type testgroup struct {
+ description string // parent test description
+ toContainer bool // copying to, or from container
+
+ // sourceSpec as specified by the user (without the container: part) - can be relative or absolute -
+ // if sourceSpec points to a file, you must use srcFileName for filename
+ sourceSpec string
+ sourceIsAFile bool // whether the provided sourceSpec points to a file or a dir
+ testCases []testcases // testcases
+}
+
+type testcases struct {
+ description string // textual description of what the test is doing
+ destinationSpec string // destination path as specified by the user (without the container: part) - can be relative or absolute
+ expect icmd.Expected // expectation
+
+ // Optional
+ catFile string // path that we "cat" - defaults to destinationSpec if not specified
+ setup func(base *testutil.Base, container string, destPath string) // additional test setup if needed
+ tearDown func() // additional cleanup if needed
+ volume func(base *testutil.Base, id string) (string, string, bool) // volume creation function if needed (should return the volume name, mountPoint, readonly flag)
+}
+
+func TestCopyToContainer(t *testing.T) {
+ t.Parallel()
+
+ testGroups := []*testgroup{
+ {
+ description: "Copying to container, SRC_PATH is a file, absolute",
+ sourceSpec: filepath.Join(string(os.PathSeparator), srcDirName, srcFileName),
+ sourceIsAFile: true,
+ toContainer: true,
+ testCases: []testcases{
+ {
+ description: "DEST_PATH does not exist, relative",
+ destinationSpec: pathDoesNotExistRelative,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute",
+ destinationSpec: pathDoesNotExistAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, relative, and ends with " + string(os.PathSeparator),
+ destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationDirMustExist.Error(),
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute, and ends with " + string(os.PathSeparator),
+ destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationDirMustExist.Error(),
+ },
+ },
+
+ {
+ description: "DEST_PATH is a file, relative",
+ destinationSpec: pathIsAFileRelative,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute",
+ destinationSpec: pathIsAFileAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative, ends with improper " + string(os.PathSeparator),
+ destinationSpec: pathIsAFileRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute, ends with improper " + string(os.PathSeparator),
+ destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ // FIXME: it is unclear why the code path with absolute (this test) versus relative (just above)
+ // yields a different error. Both should ideally be ErrCannotCopyDirToFile
+ // This is probably happening somewhere in resolve.
+ // This is not a deal killer, as both DO error with a reasonable explanation, but a bit
+ // frustrating
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative, ends with " + string(os.PathSeparator),
+ destinationSpec: pathIsADirRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute, ends with " + string(os.PathSeparator),
+ destinationSpec: pathIsADirAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a volume mount-point",
+ destinationSpec: pathIsAVolumeMount,
+ catFile: filepath.Join(pathIsAVolumeMount, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ // FIXME the way we handle volume is not right - too complicated for the test author
+ volume: func(base *testutil.Base, id string) (string, string, bool) {
+ base.Cmd("volume", "create", id).Run()
+ return id, pathIsAVolumeMount, false
+ },
+ },
+ {
+ description: "DEST_PATH is a read-only volume mount-point",
+ destinationSpec: pathIsAVolumeMount,
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrTargetIsReadOnly.Error(),
+ },
+ volume: func(base *testutil.Base, id string) (string, string, bool) {
+ base.Cmd("volume", "create", id).Run()
+ return id, pathIsAVolumeMount, true
+ },
+ },
+ },
+ },
+ {
+ description: "Copying to container, SRC_PATH is a directory",
+ sourceSpec: srcDirName,
+ toContainer: true,
+ testCases: []testcases{
+ {
+ description: "DEST_PATH does not exist, relative",
+ destinationSpec: pathDoesNotExistRelative,
+ catFile: filepath.Join(pathDoesNotExistRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute",
+ destinationSpec: pathDoesNotExistAbsolute,
+ catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, relative, and ends with " + string(os.PathSeparator),
+ destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathDoesNotExistRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute, and ends with " + string(os.PathSeparator),
+ destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative",
+ destinationSpec: pathIsAFileRelative,
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrCannotCopyDirToFile.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute",
+ destinationSpec: pathIsAFileAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrCannotCopyDirToFile.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative, ends with improper " + string(os.PathSeparator),
+ destinationSpec: pathIsAFileRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute, ends with improper " + string(os.PathSeparator),
+ destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ // FIXME: it is unclear why the code path with absolute (this test) versus relative (just above)
+ // yields a different error. Both should ideally be ErrCannotCopyDirToFile
+ // This is probably happening somewhere in resolve.
+ // This is not a deal killer, as both DO error with a reasonable explanation, but a bit
+ // frustrating
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "touch", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative, ends with " + string(os.PathSeparator),
+ destinationSpec: pathIsADirRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute, ends with " + string(os.PathSeparator),
+ destinationSpec: pathIsADirAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ },
+ },
+ {
+ description: "Copying to container, SRC_PATH is a directory ending with /.",
+ sourceSpec: srcDirName + string(os.PathSeparator) + ".",
+ toContainer: true,
+ testCases: []testcases{
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ setup: func(base *testutil.Base, container string, destPath string) {
+ base.Cmd("exec", container, "mkdir", "-p", destPath).AssertOK()
+ },
+ },
+ },
+ },
+ }
+
+ for _, tg := range testGroups {
+ cpTestHelper(t, tg)
+ }
+}
+
+func TestCopyFromContainer(t *testing.T) {
+ t.Parallel()
+
+ testGroups := []*testgroup{
+ {
+ description: "Copying from container, SRC_PATH specifies a file",
+ sourceSpec: srcFileName,
+ sourceIsAFile: true,
+ testCases: []testcases{
+ {
+ description: "DEST_PATH does not exist, relative",
+ destinationSpec: pathDoesNotExistRelative,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute",
+ destinationSpec: pathDoesNotExistAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, relative, and ends with a path separator",
+ destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationDirMustExist.Error(),
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute, and ends with a path separator",
+ destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationDirMustExist.Error(),
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative",
+ destinationSpec: pathIsAFileRelative,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute",
+ destinationSpec: pathIsAFileAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative, improperly ends with a separator",
+ destinationSpec: pathIsAFileRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute, improperly ends with a separator",
+ destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative, ending with a path separator",
+ destinationSpec: pathIsADirRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute, ending with a path separator",
+ destinationSpec: pathIsADirAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ },
+ },
+ {
+ description: "Copying from container, SRC_PATH specifies a dir",
+ sourceSpec: srcDirName,
+ testCases: []testcases{
+ {
+ description: "DEST_PATH does not exist, relative",
+ destinationSpec: pathDoesNotExistRelative,
+ catFile: filepath.Join(pathDoesNotExistRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute",
+ destinationSpec: pathDoesNotExistAbsolute,
+ catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, relative, ends with path separator",
+ destinationSpec: pathDoesNotExistRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathDoesNotExistRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH does not exist, absolute, ends with path separator",
+ destinationSpec: pathDoesNotExistAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathDoesNotExistAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative",
+ destinationSpec: pathIsAFileRelative,
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrCannotCopyDirToFile.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(filepath.Dir(destPath), dirPerm)
+ assert.NilError(t, err)
+ err = os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute",
+ destinationSpec: pathIsAFileAbsolute,
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrCannotCopyDirToFile.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(filepath.Dir(destPath), dirPerm)
+ assert.NilError(t, err)
+ err = os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, relative, improperly ends with path separator",
+ destinationSpec: pathIsAFileRelative + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(filepath.Dir(destPath), dirPerm)
+ assert.NilError(t, err)
+ err = os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a file, absolute, improperly ends with path separator",
+ destinationSpec: pathIsAFileAbsolute + string(os.PathSeparator),
+ expect: icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrDestinationIsNotADir.Error(),
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(filepath.Dir(destPath), dirPerm)
+ assert.NilError(t, err)
+ err = os.WriteFile(destPath, []byte(""), filePerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, relative, ends with path separator",
+ destinationSpec: pathIsADirRelative + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirRelative, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute, ends with path separator",
+ destinationSpec: pathIsADirAbsolute + string(os.PathSeparator),
+ catFile: filepath.Join(pathIsADirAbsolute, filepath.Base(srcDirName), srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ },
+ },
+
+ {
+ description: "SRC_PATH is a dir, with a trailing slash/dot",
+ sourceSpec: srcDirName + string(os.PathSeparator) + ".",
+ testCases: []testcases{
+ {
+ description: "DEST_PATH is a directory, relative",
+ destinationSpec: pathIsADirRelative,
+ catFile: filepath.Join(pathIsADirRelative, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ {
+ description: "DEST_PATH is a directory, absolute",
+ destinationSpec: pathIsADirAbsolute,
+ catFile: filepath.Join(pathIsADirAbsolute, srcFileName),
+ expect: icmd.Expected{
+ ExitCode: 0,
+ },
+ setup: func(base *testutil.Base, container string, destPath string) {
+ err := os.MkdirAll(destPath, dirPerm)
+ assert.NilError(t, err)
+ },
+ },
+ },
+ },
+ }
+
+ for _, tg := range testGroups {
+ cpTestHelper(t, tg)
+ }
+}
+
+func assertCatHelper(base *testutil.Base, catPath string, fileContent []byte, container string, expectedUID int, containerIsStopped bool) {
+ base.T.Logf("catPath=%q", catPath)
+ if container != "" && containerIsStopped {
+ base.Cmd("start", container).AssertOK()
+ defer base.Cmd("stop", container).AssertOK()
+ }
+
+ if container == "" {
+ got, err := os.ReadFile(catPath)
+ assert.NilError(base.T, err, "Failed reading from file")
+ assert.DeepEqual(base.T, fileContent, got)
+ st, err := os.Stat(catPath)
+ assert.NilError(base.T, err)
+ stSys := st.Sys().(*syscall.Stat_t)
+ expected := uint32(expectedUID)
+ actual := stSys.Uid
+ assert.DeepEqual(base.T, expected, actual)
+ } else {
+ base.Cmd("exec", container, "sh", "-c", "--", fmt.Sprintf("ls -lA /; echo %q; cat %q", catPath, catPath)).AssertOutContains(string(fileContent))
+ base.Cmd("exec", container, "stat", "-c", "%u", catPath).AssertOutExactly(fmt.Sprintf("%d\n", expectedUID))
+ }
+}
+
+func cpTestHelper(t *testing.T, tg *testgroup) {
+ // Get the source path
+ groupSourceSpec := tg.sourceSpec
+ groupSourceDir := groupSourceSpec
+ if tg.sourceIsAFile {
+ groupSourceDir = filepath.Dir(groupSourceSpec)
+ }
+
+ // Copy direction
+ copyToContainer := tg.toContainer
+ // Description
+ description := tg.description
+ // Test cases
+ testCases := tg.testCases
+
+ // Compute UIDs dependent on cp direction
+ var srcUID, destUID int
+ if copyToContainer {
+ srcUID = os.Geteuid()
+ destUID = srcUID
+ } else {
+ srcUID = 42
+ destUID = os.Geteuid()
+ }
+
+ t.Run(description, func(t *testing.T) {
+ t.Parallel()
+
+ for _, tc := range testCases {
+ testCase := tc
+
+ t.Run(testCase.description, func(t *testing.T) {
+ t.Parallel()
+
+ // Compute test-specific values
+ testID := testutil.Identifier(t)
+ containerRunning := testID + "-r"
+ containerStopped := testID + "-s"
+ sourceFileContent := []byte(testID)
+ tempDir := t.TempDir()
+
+ base := testutil.NewBase(t)
+ // Change working directory for commands to execute to the newly created temp directory on the host
+ // Note that ChDir won't do in a parallel context - and that setup func on the host below
+ // has to deal with that problem separately by making sure relative paths are resolved against temp
+ base.Dir = tempDir
+
+ // Prepare the specs and derived variables
+ sourceSpec := groupSourceSpec
+ destinationSpec := testCase.destinationSpec
+
+ // If the test case does not specify a catFile, start with the destination spec
+ catFile := testCase.catFile
+ if catFile == "" {
+ catFile = destinationSpec
+ }
+
+ sourceFile := filepath.Join(groupSourceDir, srcFileName)
+ if copyToContainer {
+ // Use an absolute path for evaluation
+ if !filepath.IsAbs(catFile) {
+ catFile = filepath.Join(string(os.PathSeparator), catFile)
+ }
+ // If the sourceFile is still relative, make it absolute to the temp
+ sourceFile = filepath.Join(tempDir, sourceFile)
+ // If the spec path for source on the host was absolute, make sure we put that under tempDir
+ if filepath.IsAbs(sourceSpec) {
+ sourceSpec = tempDir + sourceSpec
+ }
+ } else {
+ // If we are copying to host, we need to make sure we have an absolute path to cat, relative to temp,
+ // whether it is relative, or "absolute"
+ catFile = filepath.Join(tempDir, catFile)
+ // If the spec for destination on the host was absolute, make sure we put that under tempDir
+ if filepath.IsAbs(destinationSpec) {
+ destinationSpec = tempDir + destinationSpec
+ }
+ }
+
+ // Teardown: clean-up containers and optional volume
+ tearDown := func() {
+ base.Cmd("rm", "-f", containerRunning).Run()
+ base.Cmd("rm", "-f", containerStopped).Run()
+ if testCase.volume != nil {
+ volID, _, _ := testCase.volume(base, testID)
+ base.Cmd("volume", "rm", volID).Run()
+ }
+ }
+
+ createFileOnHost := func() {
+ // Create file on the host
+ err := os.MkdirAll(filepath.Dir(sourceFile), dirPerm)
+ assert.NilError(t, err)
+ err = os.WriteFile(sourceFile, sourceFileContent, filePerm)
+ assert.NilError(t, err)
+ }
+
+ // Setup: create volume, containers, create the source file
+ setup := func() {
+ args := []string{"run", "-d", "-w", containerCwd}
+ if testCase.volume != nil {
+ vol, mount, ro := testCase.volume(base, testID)
+ volArg := fmt.Sprintf("%s:%s", vol, mount)
+ if ro {
+ volArg += ":ro"
+ }
+ args = append(args, "-v", volArg)
+ }
+ base.Cmd(append(args, "--name", containerRunning, testutil.CommonImage, "sleep", "Inf")...).AssertOK()
+ base.Cmd(append(args, "--name", containerStopped, testutil.CommonImage, "sleep", "Inf")...).AssertOK()
+
+ if copyToContainer {
+ createFileOnHost()
+ } else {
+ // Create file content in the container
+ // Note: cd /, otherwise we end-up in the container cwd, which is NOT obeyed by cp
+ mkSrcScript := fmt.Sprintf("cd /; mkdir -p %q && echo -n %q >%q && chown %d %q", filepath.Dir(sourceFile), sourceFileContent, sourceFile, srcUID, sourceFile)
+ base.Cmd("exec", containerRunning, "sh", "-euc", mkSrcScript).AssertOK()
+ base.Cmd("exec", containerStopped, "sh", "-euc", mkSrcScript).AssertOK()
+ }
+
+ // If we have optional setup, run that now
+ if testCase.setup != nil {
+ // Some specs may come with a trailing slash (proper or improper)
+ // Setup should still work in all cases (including if its a file), and get through to the actual test
+ setupDest := destinationSpec
+ setupDest = strings.TrimSuffix(setupDest, string(os.PathSeparator))
+ if !filepath.IsAbs(setupDest) {
+ if copyToContainer {
+ setupDest = filepath.Join(string(os.PathSeparator), setupDest)
+ } else {
+ setupDest = filepath.Join(tempDir, setupDest)
+ }
+ }
+ testCase.setup(base, containerRunning, setupDest)
+ testCase.setup(base, containerStopped, setupDest)
+ }
+
+ // Stop the "stopped" container
+ base.Cmd("stop", containerStopped).AssertOK()
+ }
+
+ tearDown()
+ t.Cleanup(tearDown)
+ // If we have custom teardown, do that
+ if testCase.tearDown != nil {
+ testCase.tearDown()
+ t.Cleanup(testCase.tearDown)
+ }
+
+ // Do the setup
+ setup()
+
+ // If Docker, removes the err part of expectation
+ if testutil.GetTarget() == testutil.Docker {
+ testCase.expect.Err = ""
+ }
+
+ // Build the final src and dest specifiers, including `containerXYZ:`
+ container := ""
+ if copyToContainer {
+ container = containerRunning
+ base.Cmd("cp", sourceSpec, containerRunning+":"+destinationSpec).Assert(testCase.expect)
+ } else {
+ base.Cmd("cp", containerRunning+":"+sourceSpec, destinationSpec).Assert(testCase.expect)
+ }
+
+ // Run the actual test for the running container
+ // If we expect the op to be a success, also check the destination file
+ if testCase.expect.ExitCode == 0 {
+ assertCatHelper(base, catFile, sourceFileContent, container, destUID, false)
+ }
+
+ // When copying container > host, we get shadowing from the previous container, possibly hiding failures
+ // Solution: clear-up the tempDir
+ if copyToContainer {
+ err := os.RemoveAll(tempDir)
+ assert.NilError(t, err)
+ err = os.MkdirAll(tempDir, dirPerm)
+ assert.NilError(t, err)
+ createFileOnHost()
+ defer os.RemoveAll(tempDir)
+ }
+
+ // ... and for the stopped container
+ container = ""
+ var cmd *testutil.Cmd
+ if copyToContainer {
+ container = containerStopped
+ cmd = base.Cmd("cp", sourceSpec, containerStopped+":"+destinationSpec)
+ } else {
+ cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec)
+ }
+
+ if rootlessutil.IsRootless() && testutil.GetTarget() == testutil.Nerdctl {
+ cmd.Assert(
+ icmd.Expected{
+ ExitCode: 1,
+ Err: containerutil.ErrRootlessCannotCp.Error(),
+ })
+ return
+ }
+
+ cmd.Assert(testCase.expect)
+ if testCase.expect.ExitCode == 0 {
+ assertCatHelper(base, catFile, sourceFileContent, container, destUID, true)
+ }
+ })
+ }
+ })
+}
diff --git a/cmd/nerdctl/container/container_cp_windows.go b/cmd/nerdctl/container/container_cp_windows.go
new file mode 100644
index 00000000000..4e7d2cfd518
--- /dev/null
+++ b/cmd/nerdctl/container/container_cp_windows.go
@@ -0,0 +1,23 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import "github.com/spf13/cobra"
+
+func AddCpCommand(rootCmd *cobra.Command) {
+ // NOP
+}
diff --git a/cmd/nerdctl/container_create.go b/cmd/nerdctl/container/container_create.go
similarity index 81%
rename from cmd/nerdctl/container_create.go
rename to cmd/nerdctl/container/container_create.go
index 27280f28c99..45726e7f323 100644
--- a/cmd/nerdctl/container_create.go
+++ b/cmd/nerdctl/container/container_create.go
@@ -14,20 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"runtime"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/containerutil"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
)
-func newCreateCommand() *cobra.Command {
+func NewCreateCommand() *cobra.Command {
shortHelp := "Create a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS."
longHelp := shortHelp
switch runtime.GOOS {
@@ -53,69 +55,73 @@ func newCreateCommand() *cobra.Command {
return createCommand
}
-func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreateOptions, err error) {
- opt.Stdout = cmd.OutOrStdout()
- opt.Stderr = cmd.ErrOrStderr()
- opt.GOptions, err = processRootCmdFlags(cmd)
+func processContainerCreateOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) {
+ var err error
+ opt := types.ContainerCreateOptions{
+ Stdout: cmd.OutOrStdout(),
+ Stderr: cmd.ErrOrStderr(),
+ }
+
+ opt.GOptions, err = helpers.ProcessRootCmdFlags(cmd)
if err != nil {
- return
+ return opt, err
}
- opt.NerdctlCmd, opt.NerdctlArgs = globalFlags(cmd)
+ opt.NerdctlCmd, opt.NerdctlArgs = helpers.GlobalFlags(cmd)
// #region for basic flags
// The command `container start` doesn't support the flag `--interactive`. Set the default value of `opt.Interactive` false.
opt.Interactive = false
opt.TTY, err = cmd.Flags().GetBool("tty")
if err != nil {
- return
+ return opt, err
}
// The nerdctl create command similar to nerdctl run -d except the container is never started.
// So we keep the default value of `opt.Detach` true.
opt.Detach = true
opt.Restart, err = cmd.Flags().GetString("restart")
if err != nil {
- return
+ return opt, err
}
opt.Rm, err = cmd.Flags().GetBool("rm")
if err != nil {
- return
+ return opt, err
}
opt.Pull, err = cmd.Flags().GetString("pull")
if err != nil {
- return
+ return opt, err
}
opt.Pid, err = cmd.Flags().GetString("pid")
if err != nil {
- return
+ return opt, err
}
opt.StopSignal, err = cmd.Flags().GetString("stop-signal")
if err != nil {
- return
+ return opt, err
}
opt.StopTimeout, err = cmd.Flags().GetInt("stop-timeout")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for platform flags
opt.Platform, err = cmd.Flags().GetString("platform")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for init process flags
opt.InitProcessFlag, err = cmd.Flags().GetBool("init")
if err != nil {
- return
+ return opt, err
}
if opt.InitProcessFlag || cmd.Flags().Changed("init-binary") {
var initBinary string
initBinary, err = cmd.Flags().GetString("init-binary")
if err != nil {
- return
+ return opt, err
}
opt.InitBinary = &initBinary
}
@@ -124,97 +130,97 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
// #region for isolation flags
opt.Isolation, err = cmd.Flags().GetString("isolation")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for resource flags
opt.CPUs, err = cmd.Flags().GetFloat64("cpus")
if err != nil {
- return
+ return opt, err
}
opt.CPUQuota, err = cmd.Flags().GetInt64("cpu-quota")
if err != nil {
- return
+ return opt, err
}
opt.CPUPeriod, err = cmd.Flags().GetUint64("cpu-period")
if err != nil {
- return
+ return opt, err
}
opt.CPUShares, err = cmd.Flags().GetUint64("cpu-shares")
if err != nil {
- return
+ return opt, err
}
opt.CPUSetCPUs, err = cmd.Flags().GetString("cpuset-cpus")
if err != nil {
- return
+ return opt, err
}
opt.CPUSetMems, err = cmd.Flags().GetString("cpuset-mems")
if err != nil {
- return
+ return opt, err
}
opt.Memory, err = cmd.Flags().GetString("memory")
if err != nil {
- return
+ return opt, err
}
opt.MemoryReservationChanged = cmd.Flags().Changed("memory-reservation")
opt.MemoryReservation, err = cmd.Flags().GetString("memory-reservation")
if err != nil {
- return
+ return opt, err
}
opt.MemorySwap, err = cmd.Flags().GetString("memory-swap")
if err != nil {
- return
+ return opt, err
}
opt.MemorySwappiness64Changed = cmd.Flags().Changed("memory-swappiness")
opt.MemorySwappiness64, err = cmd.Flags().GetInt64("memory-swappiness")
if err != nil {
- return
+ return opt, err
}
opt.KernelMemoryChanged = cmd.Flag("kernel-memory").Changed
opt.KernelMemory, err = cmd.Flags().GetString("kernel-memory")
if err != nil {
- return
+ return opt, err
}
opt.OomKillDisable, err = cmd.Flags().GetBool("oom-kill-disable")
if err != nil {
- return
+ return opt, err
}
opt.OomScoreAdjChanged = cmd.Flags().Changed("oom-score-adj")
opt.OomScoreAdj, err = cmd.Flags().GetInt("oom-score-adj")
if err != nil {
- return
+ return opt, err
}
opt.PidsLimit, err = cmd.Flags().GetInt64("pids-limit")
if err != nil {
- return
+ return opt, err
}
opt.CgroupConf, err = cmd.Flags().GetStringSlice("cgroup-conf")
if err != nil {
- return
+ return opt, err
}
opt.BlkioWeight, err = cmd.Flags().GetUint16("blkio-weight")
if err != nil {
- return
+ return opt, err
}
opt.Cgroupns, err = cmd.Flags().GetString("cgroupns")
if err != nil {
- return
+ return opt, err
}
opt.CgroupParent, err = cmd.Flags().GetString("cgroup-parent")
if err != nil {
- return
+ return opt, err
}
opt.Device, err = cmd.Flags().GetStringSlice("device")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for intel RDT flags
opt.RDTClass, err = cmd.Flags().GetString("rdt-class")
if err != nil {
- return
+ return opt, err
}
// #endregion
@@ -223,75 +229,83 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
// Otherwise we will inherit permissions from the user that the containerd process is running as
opt.User, err = cmd.Flags().GetString("user")
if err != nil {
- return
+ return opt, err
}
opt.Umask = ""
if cmd.Flags().Changed("umask") {
opt.Umask, err = cmd.Flags().GetString("umask")
if err != nil {
- return
+ return opt, err
}
}
opt.GroupAdd, err = cmd.Flags().GetStringSlice("group-add")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for security flags
opt.SecurityOpt, err = cmd.Flags().GetStringArray("security-opt")
if err != nil {
- return
+ return opt, err
}
opt.CapAdd, err = cmd.Flags().GetStringSlice("cap-add")
if err != nil {
- return
+ return opt, err
}
opt.CapDrop, err = cmd.Flags().GetStringSlice("cap-drop")
if err != nil {
- return
+ return opt, err
}
opt.Privileged, err = cmd.Flags().GetBool("privileged")
if err != nil {
- return
+ return opt, err
+ }
+ opt.Systemd, err = cmd.Flags().GetString("systemd")
+ if err != nil {
+ return opt, err
}
// #endregion
// #region for runtime flags
opt.Runtime, err = cmd.Flags().GetString("runtime")
if err != nil {
- return
+ return opt, err
}
opt.Sysctl, err = cmd.Flags().GetStringArray("sysctl")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for volume flags
opt.Volume, err = cmd.Flags().GetStringArray("volume")
if err != nil {
- return
+ return opt, err
}
// tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"}
opt.Tmpfs, err = cmd.Flags().GetStringArray("tmpfs")
if err != nil {
- return
+ return opt, err
}
opt.Mount, err = cmd.Flags().GetStringArray("mount")
if err != nil {
- return
+ return opt, err
+ }
+ opt.VolumesFrom, err = cmd.Flags().GetStringArray("volumes-from")
+ if err != nil {
+ return opt, err
}
// #endregion
// #region for rootfs flags
opt.ReadOnly, err = cmd.Flags().GetBool("read-only")
if err != nil {
- return
+ return opt, err
}
opt.Rootfs, err = cmd.Flags().GetBool("rootfs")
if err != nil {
- return
+ return opt, err
}
// #endregion
@@ -299,19 +313,19 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
opt.EntrypointChanged = cmd.Flags().Changed("entrypoint")
opt.Entrypoint, err = cmd.Flags().GetStringArray("entrypoint")
if err != nil {
- return
+ return opt, err
}
opt.Workdir, err = cmd.Flags().GetString("workdir")
if err != nil {
- return
+ return opt, err
}
opt.Env, err = cmd.Flags().GetStringArray("env")
if err != nil {
- return
+ return opt, err
}
opt.EnvFile, err = cmd.Flags().GetStringSlice("env-file")
if err != nil {
- return
+ return opt, err
}
// #endregion
@@ -319,25 +333,29 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
opt.NameChanged = cmd.Flags().Changed("name")
opt.Name, err = cmd.Flags().GetString("name")
if err != nil {
- return
+ return opt, err
}
opt.Label, err = cmd.Flags().GetStringArray("label")
if err != nil {
- return
+ return opt, err
}
opt.LabelFile, err = cmd.Flags().GetStringSlice("label-file")
if err != nil {
- return
+ return opt, err
+ }
+ opt.Annotations, err = cmd.Flags().GetStringArray("annotation")
+ if err != nil {
+ return opt, err
}
opt.CidFile, err = cmd.Flags().GetString("cidfile")
if err != nil {
- return
+ return opt, err
}
opt.PidFile = ""
if cmd.Flags().Changed("pidfile") {
opt.PidFile, err = cmd.Flags().GetString("pidfile")
if err != nil {
- return
+ return opt, err
}
}
// #endregion
@@ -346,50 +364,54 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
// json-file is the built-in and default log driver for nerdctl
opt.LogDriver, err = cmd.Flags().GetString("log-driver")
if err != nil {
- return
+ return opt, err
}
opt.LogOpt, err = cmd.Flags().GetStringArray("log-opt")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for shared memory flags
opt.IPC, err = cmd.Flags().GetString("ipc")
if err != nil {
- return
+ return opt, err
}
opt.ShmSize, err = cmd.Flags().GetString("shm-size")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for gpu flags
opt.GPUs, err = cmd.Flags().GetStringArray("gpus")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for ulimit flags
opt.Ulimit, err = cmd.Flags().GetStringSlice("ulimit")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for ipfs flags
opt.IPFSAddress, err = cmd.Flags().GetString("ipfs-address")
if err != nil {
- return
+ return opt, err
}
// #endregion
// #region for image pull and verify options
- imageVerifyOpt, err := processImageVerifyOptions(cmd)
+ imageVerifyOpt, err := helpers.ProcessImageVerifyOptions(cmd)
+ if err != nil {
+ return opt, err
+ }
+ quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
- return
+ return opt, err
}
opt.ImagePullOpt = types.ImagePullOptions{
GOptions: opt.GOptions,
@@ -397,6 +419,7 @@ func processContainerCreateOptions(cmd *cobra.Command) (opt types.ContainerCreat
IPFSAddress: opt.IPFSAddress,
Stdout: opt.Stdout,
Stderr: opt.Stderr,
+ Quiet: quiet,
}
// #endregion
@@ -423,7 +446,7 @@ func createAction(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to load networking flags: %s", err)
}
- netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags)
+ netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags, client)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go
new file mode 100644
index 00000000000..a4b3bc64fa2
--- /dev/null
+++ b/cmd/nerdctl/container/container_create_linux_test.go
@@ -0,0 +1,326 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/opencontainers/go-digest"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/containerd/v2/defaults"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestCreateWithLabel(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+
+ base.Cmd("create", "--name", tID, "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo").AssertOK()
+ defer base.Cmd("rm", "-f", tID).Run()
+ inspect := base.InspectContainer(tID)
+ assert.Equal(base.T, "bar", inspect.Config.Labels["foo"])
+ // the label `maintainer`` is defined by image
+ assert.Equal(base.T, "NGINX Docker Maintainers ", inspect.Config.Labels["maintainer"])
+}
+
+func TestCreateWithMACAddress(t *testing.T) {
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+ networkBridge := "testNetworkBridge" + tID
+ networkMACvlan := "testNetworkMACvlan" + tID
+ networkIPvlan := "testNetworkIPvlan" + tID
+
+ tearDown := func() {
+ base.Cmd("network", "rm", networkBridge).Run()
+ base.Cmd("network", "rm", networkMACvlan).Run()
+ base.Cmd("network", "rm", networkIPvlan).Run()
+ }
+
+ tearDown()
+ t.Cleanup(tearDown)
+
+ base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
+ base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
+ base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
+
+ defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage).
+ CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).
+ Run().Stdout()
+
+ passedMac := "we expect the generated mac on the output"
+ tests := []struct {
+ Network string
+ WantErr bool
+ Expect string
+ }{
+ {"host", false, defaultMac}, // anything but the actual address being passed
+ {"none", false, ""},
+ {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container"
+ {"bridge", false, passedMac},
+ {networkBridge, false, passedMac},
+ {networkMACvlan, false, passedMac},
+ {networkIPvlan, true, "not support"},
+ }
+ for i, test := range tests {
+ containerName := fmt.Sprintf("%s_%d", tID, i)
+ testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect)
+ expect := test.Expect
+ network := test.Network
+ wantErr := test.WantErr
+ t.Run(testName, func(tt *testing.T) {
+ tt.Parallel()
+
+ macAddress, err := nettestutil.GenerateMACAddress()
+ if err != nil {
+ tt.Errorf("failed to generate MAC address: %s", err)
+ }
+ if expect == passedMac {
+ expect = macAddress
+ }
+ tearDown := func() {
+ base.Cmd("rm", "-f", containerName).Run()
+ }
+ tearDown()
+ tt.Cleanup(tearDown)
+ // This is currently blocked by https://github.com/containerd/nerdctl/pull/3104
+ // res := base.Cmd("create", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage).Run()
+ res := base.Cmd("create", "--network", network, "--name", containerName,
+ "--mac-address", macAddress, testutil.CommonImage,
+ "sh", "-c", "--", "ip addr show").Run()
+
+ if !wantErr {
+ assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res)
+ // This is currently blocked by: https://github.com/containerd/nerdctl/pull/3104
+ // res = base.Cmd("start", "-i", containerName).
+ // CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run()
+ res = base.Cmd("start", "-a", containerName).Run()
+ // FIXME: flaky - this has failed on the CI once, with the output NOT containing anything
+ // https://github.com/containerd/nerdctl/actions/runs/11392051487/job/31697214002?pr=3535#step:7:271
+ assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout()))
+ assert.Assert(t, res.ExitCode == 0, "Command should have succeeded")
+ } else {
+ if testutil.GetTarget() == testutil.Docker &&
+ (network == networkIPvlan || network == "container:whatever"+tID) {
+ // unlike nerdctl
+ // when using network ipvlan or container in Docker
+ // it delays fail on executing start command
+ assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res)
+ res = base.Cmd("start", "-i", "-a", containerName).
+ CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run()
+ }
+
+ // See https://github.com/containerd/nerdctl/issues/3101
+ if testutil.GetTarget() == testutil.Docker &&
+ (network == networkBridge) {
+ expect = ""
+ }
+ if expect != "" {
+ assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined()))
+ } else {
+ assert.Assert(t, res.Combined() == "", fmt.Sprintf("expected output to be empty: %q", res.Combined()))
+ }
+ assert.Assert(t, res.ExitCode != 0, "Command should have failed", res)
+ }
+ })
+ }
+}
+
+func TestCreateWithTty(t *testing.T) {
+ base := testutil.NewBase(t)
+ imageName := testutil.CommonImage
+ withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t)
+ withTtyContainerName := "with-terminal-" + testutil.Identifier(t)
+
+ // without -t, fail
+ base.Cmd("create", "--name", withoutTtyContainerName, imageName, "stty").AssertOK()
+ base.Cmd("start", withoutTtyContainerName).AssertOK()
+ defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK()
+ base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty")
+ withoutTtyContainer := base.InspectContainer(withoutTtyContainerName)
+ assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode)
+
+ // with -t, success
+ base.Cmd("create", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK()
+ base.Cmd("start", withTtyContainerName).AssertOK()
+ defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK()
+ base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;")
+ withTtyContainer := base.InspectContainer(withTtyContainerName)
+ assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
+}
+
+// TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993
+func TestIssue2993(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = test.Not(nerdtest.Docker)
+
+ const (
+ containersPathKey = "containersPath"
+ etchostsPathKey = "etchostsPath"
+ )
+
+ getAddrHash := func(addr string) string {
+ const addrHashLen = 8
+
+ d := digest.SHA256.FromString(addr)
+ h := d.Encoded()[0:addrHashLen]
+
+ return h
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Issue #2993 - nerdctl no longer leaks containers and etchosts directories and files when container creation fails.",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dataRoot := data.TempDir()
+
+ helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity)
+
+ h := getAddrHash(defaults.DefaultAddress)
+ dataStore := filepath.Join(dataRoot, h)
+
+ namespace := string(helpers.Read(nerdtest.Namespace))
+
+ containersPath := filepath.Join(dataStore, "containers", namespace)
+ containersDirs, err := os.ReadDir(containersPath)
+ assert.NilError(t, err)
+ assert.Equal(t, len(containersDirs), 1)
+
+ etchostsPath := filepath.Join(dataStore, "etchosts", namespace)
+ etchostsDirs, err := os.ReadDir(etchostsPath)
+ assert.NilError(t, err)
+ assert.Equal(t, len(etchostsDirs), 1)
+
+ data.Set(containersPathKey, containersPath)
+ data.Set(etchostsPathKey, etchostsPath)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "--data-root", data.TempDir(), "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--data-root", data.TempDir(), "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity)
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("is already used by ID")},
+ Output: func(stdout string, info string, t *testing.T) {
+ containersDirs, err := os.ReadDir(data.Get(containersPathKey))
+ assert.NilError(t, err)
+ assert.Equal(t, len(containersDirs), 1)
+
+ etchostsDirs, err := os.ReadDir(data.Get(etchostsPathKey))
+ assert.NilError(t, err)
+ assert.Equal(t, len(etchostsDirs), 1)
+ },
+ }
+ },
+ },
+ {
+ Description: "Issue #2993 - nerdctl no longer leaks containers and etchosts directories and files when containers are removed.",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dataRoot := data.TempDir()
+
+ helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity)
+
+ h := getAddrHash(defaults.DefaultAddress)
+ dataStore := filepath.Join(dataRoot, h)
+
+ namespace := string(helpers.Read(nerdtest.Namespace))
+
+ containersPath := filepath.Join(dataStore, "containers", namespace)
+ containersDirs, err := os.ReadDir(containersPath)
+ assert.NilError(t, err)
+ assert.Equal(t, len(containersDirs), 1)
+
+ etchostsPath := filepath.Join(dataStore, "etchosts", namespace)
+ etchostsDirs, err := os.ReadDir(etchostsPath)
+ assert.NilError(t, err)
+ assert.Equal(t, len(etchostsDirs), 1)
+
+ data.Set(containersPathKey, containersPath)
+ data.Set(etchostsPathKey, etchostsPath)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("--data-root", data.TempDir(), "rm", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("--data-root", data.TempDir(), "rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ containersDirs, err := os.ReadDir(data.Get(containersPathKey))
+ assert.NilError(t, err)
+ assert.Equal(t, len(containersDirs), 0)
+
+ etchostsDirs, err := os.ReadDir(data.Get(etchostsPathKey))
+ assert.NilError(t, err)
+ assert.Equal(t, len(etchostsDirs), 0)
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestCreateFromOCIArchive(t *testing.T) {
+ testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
+
+ // Docker does not support creating containers from OCI archive.
+ testutil.DockerIncompatible(t)
+
+ base := testutil.NewBase(t)
+ imageName := testutil.Identifier(t)
+ containerName := testutil.Identifier(t)
+
+ teardown := func() {
+ base.Cmd("rm", "-f", containerName).Run()
+ base.Cmd("rmi", "-f", imageName).Run()
+ }
+ defer teardown()
+ teardown()
+
+ const sentinel = "test-nerdctl-create-from-oci-archive"
+ dockerfile := fmt.Sprintf(`FROM %s
+ CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)
+
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
+ tag := fmt.Sprintf("%s:latest", imageName)
+ tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)
+
+ base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
+ base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK()
+ base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive")
+}
diff --git a/cmd/nerdctl/container/container_create_test.go b/cmd/nerdctl/container/container_create_test.go
new file mode 100644
index 00000000000..6885a1e64aa
--- /dev/null
+++ b/cmd/nerdctl/container/container_create_test.go
@@ -0,0 +1,127 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "encoding/json"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestCreate(t *testing.T) {
+ testCase := nerdtest.Setup()
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("create", "--name", data.Identifier("container"), testutil.CommonImage, "echo", "foo")
+ data.Set("cID", data.Identifier("container"))
+ }
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("container"))
+ }
+
+ testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3717")
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "ps -a",
+ NoParallel: true,
+ Command: test.Command("ps", "-a"),
+ // FIXME: this might get a false positive if other tests have created a container
+ Expected: test.Expects(0, nil, test.Contains("Created")),
+ },
+ {
+ Description: "start",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("start", data.Get("cID"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "logs",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("logs", data.Get("cID"))
+ },
+ Expected: test.Expects(0, nil, test.Contains("foo")),
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestCreateHyperVContainer(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.HyperV
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("create", "--isolation", "hyperv", "--name", data.Identifier("container"), testutil.CommonImage, "echo", "foo")
+ data.Set("cID", data.Identifier("container"))
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("container"))
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "ps -a",
+ NoParallel: true,
+ Command: test.Command("ps", "-a"),
+ // FIXME: this might get a false positive if other tests have created a container
+ Expected: test.Expects(0, nil, test.Contains("Created")),
+ },
+ {
+ Description: "start",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("start", data.Get("cID"))
+ ran := false
+ for i := 0; i < 10 && !ran; i++ {
+ helpers.Command("container", "inspect", data.Get("cID")).
+ Run(&test.Expected{
+ ExitCode: test.ExitCodeNoCheck,
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Container
+ err := json.Unmarshal([]byte(stdout), &dc)
+ if err != nil || len(dc) == 0 {
+ return
+ }
+ assert.Equal(t, len(dc), 1, "Unexpectedly got multiple results\n"+info)
+ ran = dc[0].State.Status == "exited"
+ },
+ })
+ time.Sleep(time.Second)
+ }
+ assert.Assert(t, ran, "container did not ran after 10 seconds")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("logs", data.Get("cID"))
+ },
+ Expected: test.Expects(0, nil, test.Contains("foo")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container/container_diff.go b/cmd/nerdctl/container/container_diff.go
new file mode 100644
index 00000000000..9b3e5be47eb
--- /dev/null
+++ b/cmd/nerdctl/container/container_diff.go
@@ -0,0 +1,231 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/opencontainers/image-spec/identity"
+ "github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/leases"
+ "github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/continuity/fs"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+)
+
+func NewDiffCommand() *cobra.Command {
+ var diffCommand = &cobra.Command{
+ Use: "diff [CONTAINER]",
+ Short: "Inspect changes to files or directories on a container's filesystem",
+ Args: cobra.MinimumNArgs(1),
+ RunE: diffAction,
+ ValidArgsFunction: diffShellComplete,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+ return diffCommand
+}
+
+func processContainerDiffOptions(cmd *cobra.Command) (types.ContainerDiffOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.ContainerDiffOptions{}, err
+ }
+
+ return types.ContainerDiffOptions{
+ Stdout: cmd.OutOrStdout(),
+ GOptions: globalOptions,
+ }, nil
+}
+
+func diffAction(cmd *cobra.Command, args []string) error {
+ options, err := processContainerDiffOptions(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
+
+ walker := &containerwalker.ContainerWalker{
+ Client: client,
+ OnFound: func(ctx context.Context, found containerwalker.Found) error {
+ if found.MatchCount > 1 {
+ return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ changes, err := getChanges(ctx, client, found.Container)
+ if err != nil {
+ return err
+ }
+
+ for _, change := range changes {
+ switch change.Kind {
+ case fs.ChangeKindAdd:
+ fmt.Fprintln(options.Stdout, "A", change.Path)
+ case fs.ChangeKindModify:
+ fmt.Fprintln(options.Stdout, "C", change.Path)
+ case fs.ChangeKindDelete:
+ fmt.Fprintln(options.Stdout, "D", change.Path)
+ default:
+ }
+ }
+
+ return nil
+ },
+ }
+
+ container := args[0]
+
+ n, err := walker.Walk(ctx, container)
+ if err != nil {
+ return err
+ } else if n == 0 {
+ return fmt.Errorf("no such container %s", container)
+ }
+ return nil
+}
+
+func getChanges(ctx context.Context, client *containerd.Client, container containerd.Container) ([]fs.Change, error) {
+ id := container.ID()
+ info, err := container.Info(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ var (
+ snName = info.Snapshotter
+ sn = client.SnapshotService(snName)
+ )
+
+ mounts, err := sn.Mounts(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ // NOTE: Moby uses provided rootfs to run container. It doesn't support
+ // to commit container created by moby.
+ baseImgWithoutPlatform, err := client.ImageService().Get(ctx, info.Image)
+ if err != nil {
+ return nil, fmt.Errorf("container %q lacks image (wasn't created by nerdctl?): %w", id, err)
+ }
+ platformLabel := info.Labels[labels.Platform]
+ if platformLabel == "" {
+ platformLabel = platforms.DefaultString()
+ log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel)
+ }
+ ocispecPlatform, err := platforms.Parse(platformLabel)
+ if err != nil {
+ return nil, err
+ }
+ log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform))
+ platformMC := platforms.Only(ocispecPlatform)
+ baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC)
+
+ baseImgConfig, _, err := imgutil.ReadImageConfig(ctx, baseImg)
+ if err != nil {
+ return nil, err
+ }
+
+ // Don't gc me and clean the dirty data after 1 hour!
+ ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create lease for diff: %w", err)
+ }
+ defer done(ctx)
+
+ rootfsID := identity.ChainID(baseImgConfig.RootFS.DiffIDs).String()
+
+ randomID := idgen.GenerateID()
+ parent, err := sn.View(ctx, randomID, rootfsID)
+ if err != nil {
+ return nil, err
+ }
+ defer sn.Remove(ctx, randomID)
+
+ var changes []fs.Change
+ err = mount.WithReadonlyTempMount(ctx, parent, func(lower string) error {
+ return mount.WithReadonlyTempMount(ctx, mounts, func(upper string) error {
+ return fs.Changes(ctx, lower, upper, func(ck fs.ChangeKind, s string, fi os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ changes = appendChanges(changes, fs.Change{
+ Kind: ck,
+ Path: s,
+ })
+ return nil
+ })
+ })
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return changes, err
+}
+
+func appendChanges(changes []fs.Change, fsChange fs.Change) []fs.Change {
+ newDir, _ := filepath.Split(fsChange.Path)
+ newDirPath := filepath.SplitList(newDir)
+
+ if len(changes) == 0 {
+ for i := 1; i < len(newDirPath); i++ {
+ changes = append(changes, fs.Change{
+ Kind: fs.ChangeKindModify,
+ Path: filepath.Join(newDirPath[:i+1]...),
+ })
+ }
+ return append(changes, fsChange)
+ }
+ last := changes[len(changes)-1]
+ lastDir, _ := filepath.Split(last.Path)
+ lastDirPath := filepath.SplitList(lastDir)
+ for i := range newDirPath {
+ if len(lastDirPath) > i && lastDirPath[i] == newDirPath[i] {
+ continue
+ }
+ changes = append(changes, fs.Change{
+ Kind: fs.ChangeKindModify,
+ Path: filepath.Join(newDirPath[:i+1]...),
+ })
+ }
+ return append(changes, fsChange)
+}
+
+func diffShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ // show container names
+ return completion.ContainerNames(cmd, nil)
+}
diff --git a/cmd/nerdctl/container/container_diff_test.go b/cmd/nerdctl/container/container_diff_test.go
new file mode 100644
index 00000000000..b47631dd600
--- /dev/null
+++ b/cmd/nerdctl/container/container_diff_test.go
@@ -0,0 +1,59 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestDiff(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // It is unclear why this is failing with docker when run in parallel
+ // Obviously some other container test is interfering
+ if nerdtest.IsDocker() {
+ testCase.NoParallel = true
+ }
+
+ testCase.Require = test.Not(test.Windows)
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage,
+ "sh", "-euxc", "touch /a; touch /bin/b; rm /bin/base64")
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("diff", data.Identifier())
+ }
+
+ testCase.Expected = test.Expects(0, nil, test.All(
+ test.Contains("A /a"),
+ test.Contains("C /bin"),
+ test.Contains("A /bin/b"),
+ test.Contains("D /bin/base64"),
+ ))
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_exec.go b/cmd/nerdctl/container/container_exec.go
similarity index 88%
rename from cmd/nerdctl/container_exec.go
rename to cmd/nerdctl/container/container_exec.go
index 6b3447cd94b..fc6f0c0c6ac 100644
--- a/cmd/nerdctl/container_exec.go
+++ b/cmd/nerdctl/container/container_exec.go
@@ -14,19 +14,23 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newExecCommand() *cobra.Command {
+func NewExecCommand() *cobra.Command {
var execCommand = &cobra.Command{
Use: "exec [flags] CONTAINER COMMAND [ARG...]",
Args: cobra.MinimumNArgs(2),
@@ -52,7 +56,8 @@ func newExecCommand() *cobra.Command {
}
func processExecCommandOptions(cmd *cobra.Command) (types.ContainerExecOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ // We do not check if we have a terminal here, as container.Exec calling console.Current will ensure that
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerExecOptions{}, err
}
@@ -145,7 +150,7 @@ func execShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st == containerd.Running
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
diff --git a/cmd/nerdctl/container_exec_linux_test.go b/cmd/nerdctl/container/container_exec_linux_test.go
similarity index 91%
rename from cmd/nerdctl/container_exec_linux_test.go
rename to cmd/nerdctl/container/container_exec_linux_test.go
index 37ffcb69d06..19113781ebc 100644
--- a/cmd/nerdctl/container_exec_linux_test.go
+++ b/cmd/nerdctl/container/container_exec_linux_test.go
@@ -14,12 +14,13 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestExecWithUser(t *testing.T) {
@@ -28,7 +29,7 @@ func TestExecWithUser(t *testing.T) {
testContainer := testutil.Identifier(t)
defer base.Cmd("rm", "-f", testContainer).Run()
- base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
base.EnsureContainerStarted(testContainer)
testCases := map[string]string{
@@ -59,7 +60,7 @@ func TestExecTTY(t *testing.T) {
testContainer := testutil.Identifier(t)
defer base.Cmd("rm", "-f", testContainer).Run()
- base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
const sttyPartialOutput = "speed 38400 baud"
// unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
diff --git a/cmd/nerdctl/container_exec_test.go b/cmd/nerdctl/container/container_exec_test.go
similarity index 96%
rename from cmd/nerdctl/container_exec_test.go
rename to cmd/nerdctl/container/container_exec_test.go
index b96cc74756a..de58789ab77 100644
--- a/cmd/nerdctl/container_exec_test.go
+++ b/cmd/nerdctl/container/container_exec_test.go
@@ -14,16 +14,15 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
- "os"
"runtime"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestExec(t *testing.T) {
@@ -80,7 +79,7 @@ func TestExecEnv(t *testing.T) {
base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK()
base.EnsureContainerStarted(testContainer)
- base.Env = append(os.Environ(), "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
+ base.Env = append(base.Env, "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
base.Cmd("exec",
"--env", "FOO=foo1,foo2",
"--env", "BAR=bar1 bar2",
diff --git a/cmd/nerdctl/container_inspect.go b/cmd/nerdctl/container/container_inspect.go
similarity index 81%
rename from cmd/nerdctl/container_inspect.go
rename to cmd/nerdctl/container/container_inspect.go
index 0124a93dcda..6dae2cbf613 100644
--- a/cmd/nerdctl/container_inspect.go
+++ b/cmd/nerdctl/container/container_inspect.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
func newContainerInspectCommand() *cobra.Command {
@@ -38,6 +40,8 @@ func newContainerInspectCommand() *cobra.Command {
SilenceErrors: true,
}
containerInspectCommand.Flags().String("mode", "dockercompat", `Inspect mode, "dockercompat" for Docker-compatible output, "native" for containerd-native output`)
+ containerInspectCommand.Flags().BoolP("size", "s", false, "Display total file sizes")
+
containerInspectCommand.RegisterFlagCompletionFunc("mode", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"dockercompat", "native"}, cobra.ShellCompDirectiveNoFileComp
})
@@ -53,8 +57,8 @@ var validModeType = map[string]bool{
"dockercompat": true,
}
-func processContainerInspectOptions(cmd *cobra.Command) (opt types.ContainerInspectOptions, err error) {
- globalOptions, err := processRootCmdFlags(cmd)
+func ProcessContainerInspectOptions(cmd *cobra.Command) (opt types.ContainerInspectOptions, err error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return
}
@@ -71,16 +75,22 @@ func processContainerInspectOptions(cmd *cobra.Command) (opt types.ContainerInsp
return
}
+ size, err := cmd.Flags().GetBool("size")
+ if err != nil {
+ return
+ }
+
return types.ContainerInspectOptions{
GOptions: globalOptions,
Format: format,
Mode: mode,
+ Size: size,
Stdout: cmd.OutOrStdout(),
}, nil
}
func containerInspectAction(cmd *cobra.Command, args []string) error {
- opt, err := processContainerInspectOptions(cmd)
+ opt, err := ProcessContainerInspectOptions(cmd)
if err != nil {
return err
}
@@ -95,5 +105,5 @@ func containerInspectAction(cmd *cobra.Command, args []string) error {
func containerInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show container names
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
diff --git a/cmd/nerdctl/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go
similarity index 85%
rename from cmd/nerdctl/container_inspect_linux_test.go
rename to cmd/nerdctl/container/container_inspect_linux_test.go
index 6eb0ba9b2f1..2fadc2b5048 100644
--- a/cmd/nerdctl/container_inspect_linux_test.go
+++ b/cmd/nerdctl/container/container_inspect_linux_test.go
@@ -14,17 +14,19 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/testutil"
"github.com/docker/go-connections/nat"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestContainerInspectContainsPortConfig(t *testing.T) {
@@ -156,6 +158,25 @@ func TestContainerInspectContainsLabel(t *testing.T) {
assert.Equal(base.T, "bar", lbs["bar"])
}
+func TestContainerInspectContainsInternalLabel(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ testContainer := testutil.Identifier(t)
+
+ base := testutil.NewBase(t)
+ defer base.Cmd("rm", "-f", testContainer).Run()
+
+ base.Cmd("run", "-d", "--name", testContainer, "--mount", "type=bind,src=/tmp,dst=/app,readonly=false,bind-propagation=rprivate", testutil.NginxAlpineImage).AssertOK()
+ base.EnsureContainerStarted(testContainer)
+ inspect := base.InspectContainer(testContainer)
+ lbs := inspect.Config.Labels
+
+ // TODO: add more internal labels testcases
+ labelMount := lbs[labels.Mounts]
+ expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]"
+ assert.Equal(base.T, expectedLabelMount, labelMount)
+}
+
func TestContainerInspectState(t *testing.T) {
t.Parallel()
testContainer := testutil.Identifier(t)
diff --git a/cmd/nerdctl/container_inspect_windows_test.go b/cmd/nerdctl/container/container_inspect_windows_test.go
similarity index 96%
rename from cmd/nerdctl/container_inspect_windows_test.go
rename to cmd/nerdctl/container/container_inspect_windows_test.go
index 5ba9adbb08a..8feb7fcd52d 100644
--- a/cmd/nerdctl/container_inspect_windows_test.go
+++ b/cmd/nerdctl/container/container_inspect_windows_test.go
@@ -14,13 +14,14 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestInspectProcessContainerContainsLabel(t *testing.T) {
diff --git a/cmd/nerdctl/container_kill.go b/cmd/nerdctl/container/container_kill.go
similarity index 79%
rename from cmd/nerdctl/container_kill.go
rename to cmd/nerdctl/container/container_kill.go
index fdf8e488c72..154cec0e23f 100644
--- a/cmd/nerdctl/container_kill.go
+++ b/cmd/nerdctl/container/container_kill.go
@@ -14,17 +14,21 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newKillCommand() *cobra.Command {
+func NewKillCommand() *cobra.Command {
var killCommand = &cobra.Command{
Use: "kill [flags] CONTAINER [CONTAINER, ...]",
Short: "Kill one or more running containers",
@@ -39,7 +43,7 @@ func newKillCommand() *cobra.Command {
}
func killAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -68,5 +72,5 @@ func killShellComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobr
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container/container_kill_linux_test.go b/cmd/nerdctl/container/container_kill_linux_test.go
new file mode 100644
index 00000000000..0e7d3fc3f62
--- /dev/null
+++ b/cmd/nerdctl/container/container_kill_linux_test.go
@@ -0,0 +1,79 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/coreos/go-iptables/iptables"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables"
+)
+
+// TestKillCleanupForwards runs a container that exposes a port and then kill it.
+// The test checks that the kill command effectively clean up
+// the iptables forwards creted from the run.
+func TestKillCleanupForwards(t *testing.T) {
+ const (
+ hostPort = 9999
+ testContainerName = "ngx"
+ )
+ base := testutil.NewBase(t)
+ defer func() {
+ base.Cmd("rm", "-f", testContainerName).Run()
+ }()
+
+ // skip if rootless
+ if rootlessutil.IsRootless() {
+ t.Skip("pkg/testutil/iptables does not support rootless")
+ }
+
+ ipt, err := iptables.New()
+ assert.NilError(t, err)
+
+ containerID := base.Cmd("run", "-d",
+ "--restart=no",
+ "--name", testContainerName,
+ "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort),
+ testutil.NginxAlpineImage).Run().Stdout()
+ containerID = strings.TrimSuffix(containerID, "\n")
+
+ containerIP := base.Cmd("inspect",
+ "-f",
+ "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'",
+ testContainerName).Run().Stdout()
+ containerIP = strings.ReplaceAll(containerIP, "'", "")
+ containerIP = strings.TrimSuffix(containerIP, "\n")
+
+ // define iptables chain name depending on the target (docker/nerdctl)
+ var chain string
+ if testutil.GetTarget() == testutil.Docker {
+ chain = "DOCKER"
+ } else {
+ redirectChain := "CNI-HOSTPORT-DNAT"
+ chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID)
+ }
+ assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true)
+
+ base.Cmd("kill", testContainerName).AssertOK()
+ assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false)
+}
diff --git a/cmd/nerdctl/container/container_list.go b/cmd/nerdctl/container/container_list.go
new file mode 100644
index 00000000000..c9f0d3bd2bb
--- /dev/null
+++ b/cmd/nerdctl/container/container_list.go
@@ -0,0 +1,235 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "text/tabwriter"
+ "text/template"
+
+ "github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+)
+
+func NewPsCommand() *cobra.Command {
+ var psCommand = &cobra.Command{
+ Use: "ps",
+ Args: cobra.NoArgs,
+ Short: "List containers",
+ RunE: psAction,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+ psCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
+ psCommand.Flags().IntP("last", "n", -1, "Show n last created containers (includes all states)")
+ psCommand.Flags().BoolP("latest", "l", false, "Show the latest created container (includes all states)")
+ psCommand.Flags().Bool("no-trunc", false, "Don't truncate output")
+ psCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs")
+ psCommand.Flags().BoolP("size", "s", false, "Display total file sizes")
+
+ // Alias "-f" is reserved for "--filter"
+ psCommand.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}', 'wide'")
+ psCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp
+ })
+ psCommand.Flags().StringSliceP("filter", "f", nil, "Filter matches containers based on given conditions. When specifying the condition 'status', it filters all containers")
+ return psCommand
+}
+
+func processOptions(cmd *cobra.Command) (types.ContainerListOptions, FormattingAndPrintingOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ all, err := cmd.Flags().GetBool("all")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ latest, err := cmd.Flags().GetBool("latest")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+
+ lastN, err := cmd.Flags().GetInt("last")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ if lastN == -1 && latest {
+ lastN = 1
+ }
+
+ filters, err := cmd.Flags().GetStringSlice("filter")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+
+ noTrunc, err := cmd.Flags().GetBool("no-trunc")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ trunc := !noTrunc
+
+ quiet, err := cmd.Flags().GetBool("quiet")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ format, err := cmd.Flags().GetString("format")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+
+ size := false
+ if !quiet {
+ size, err = cmd.Flags().GetBool("size")
+ if err != nil {
+ return types.ContainerListOptions{}, FormattingAndPrintingOptions{}, err
+ }
+ }
+
+ return types.ContainerListOptions{
+ GOptions: globalOptions,
+ All: all,
+ LastN: lastN,
+ Truncate: trunc,
+ Size: size || (format == "wide" && !quiet),
+ Filters: filters,
+ }, FormattingAndPrintingOptions{
+ Stdout: cmd.OutOrStdout(),
+ Quiet: quiet,
+ Format: format,
+ Size: size,
+ }, nil
+}
+
+func psAction(cmd *cobra.Command, args []string) error {
+ clOpts, fpOpts, err := processOptions(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), clOpts.GOptions.Namespace, clOpts.GOptions.Address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
+
+ containers, err := container.List(ctx, client, clOpts)
+ if err != nil {
+ return err
+ }
+
+ return formatAndPrintContainerInfo(containers, fpOpts)
+}
+
+// FormattingAndPrintingOptions specifies options for formatting and printing of `nerdctl (container) list`.
+type FormattingAndPrintingOptions struct {
+ Stdout io.Writer
+ // Only display container IDs.
+ Quiet bool
+ // Format the output using the given Go template (e.g., '{{json .}}', 'table', 'wide').
+ Format string
+ // Display total file sizes.
+ Size bool
+}
+
+func formatAndPrintContainerInfo(containers []container.ListItem, options FormattingAndPrintingOptions) error {
+ w := options.Stdout
+ var (
+ wide bool
+ tmpl *template.Template
+ )
+ switch options.Format {
+ case "", "table":
+ w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
+ if !options.Quiet {
+ printHeader := "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES"
+ if options.Size {
+ printHeader += "\tSIZE"
+ }
+ fmt.Fprintln(w, printHeader)
+ }
+ case "raw":
+ return errors.New("unsupported format: \"raw\"")
+ case "wide":
+ w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
+ if !options.Quiet {
+ fmt.Fprintln(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE")
+ wide = true
+ }
+ default:
+ if options.Quiet {
+ return errors.New("format and quiet must not be specified together")
+ }
+ var err error
+ tmpl, err = formatter.ParseTemplate(options.Format)
+ if err != nil {
+ return err
+ }
+ }
+
+ for _, c := range containers {
+ if tmpl != nil {
+ var b bytes.Buffer
+ if err := tmpl.Execute(&b, &c); err != nil {
+ return err
+ }
+ if _, err := fmt.Fprintln(w, b.String()); err != nil {
+ return err
+ }
+ } else if options.Quiet {
+ if _, err := fmt.Fprintln(w, c.ID); err != nil {
+ return err
+ }
+ } else {
+ format := "%s\t%s\t%s\t%s\t%s\t%s\t%s"
+ args := []interface{}{
+ c.ID,
+ c.Image,
+ c.Command,
+ formatter.TimeSinceInHuman(c.CreatedAt),
+ c.Status,
+ c.Ports,
+ c.Names,
+ }
+ if wide {
+ format += "\t%s\t%s\t%s\n"
+ args = append(args, c.Runtime, c.Platform, c.Size)
+ } else if options.Size {
+ format += "\t%s\n"
+ args = append(args, c.Size)
+ } else {
+ format += "\n"
+ }
+ if _, err := fmt.Fprintf(w, format, args...); err != nil {
+ return err
+ }
+ }
+
+ }
+ if f, ok := w.(formatter.Flusher); ok {
+ return f.Flush()
+ }
+ return nil
+}
diff --git a/cmd/nerdctl/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go
similarity index 86%
rename from cmd/nerdctl/container_list_linux_test.go
rename to cmd/nerdctl/container/container_list_linux_test.go
index d7b04a3f005..79df967332a 100644
--- a/cmd/nerdctl/container_list_linux_test.go
+++ b/cmd/nerdctl/container/container_list_linux_test.go
@@ -14,20 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
"fmt"
"os"
+ "slices"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/containerd/nerdctl/pkg/tabutil"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/tabutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
type psTestContainer struct {
@@ -37,7 +39,8 @@ type psTestContainer struct {
network string
}
-func preparePsTestContainer(t *testing.T, identity string, restart bool) (*testutil.Base, psTestContainer) {
+// When keepAlive is false, the container will exit immediately with status 1.
+func preparePsTestContainer(t *testing.T, identity string, keepAlive bool) (*testutil.Base, psTestContainer) {
base := testutil.NewBase(t)
base.Cmd("pull", testutil.CommonImage).AssertOK()
@@ -82,21 +85,23 @@ func preparePsTestContainer(t *testing.T, identity string, restart bool) (*testu
"-v", mnt1,
"-v", mnt2,
"--net", testContainerName,
- testutil.CommonImage,
- "top",
}
- if !restart {
- args = append(args, "--restart=no")
+ if keepAlive {
+ args = append(args, testutil.CommonImage, "top")
+ } else {
+ args = append(args, "--restart=no", testutil.CommonImage, "false")
}
base.Cmd(args...).AssertOK()
- if restart {
+ if keepAlive {
base.EnsureContainerStarted(testContainerName)
+ } else {
+ base.EnsureContainerExited(testContainerName, 1)
}
// dd if=/dev/zero of=test_file bs=1M count=25
// let the container occupy 25MiB space.
- if restart {
+ if keepAlive {
base.Cmd("exec", testContainerName, "dd", "if=/dev/zero", "of=/test_file", "bs=1M", "count=25").AssertOK()
}
volumes := []string{}
@@ -223,6 +228,24 @@ func TestContainerListWithLabels(t *testing.T) {
})
}
+func TestContainerListWithNames(t *testing.T) {
+ base, testContainer := preparePsTestContainer(t, "listWithNames", true)
+
+ // hope there are no tests running parallel
+ base.Cmd("ps", "-n", "1", "--format", "{{.Names}}").AssertOutWithFunc(func(stdout string) error {
+
+ // An example of nerdctl ps --format "{{.Names}}"
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ if len(lines) != 1 {
+ return fmt.Errorf("expected 1 line, got %d", len(lines))
+ }
+
+ assert.Equal(t, lines[0], testContainer.name)
+
+ return nil
+ })
+}
+
func TestContainerListWithFilter(t *testing.T) {
base, testContainerA := preparePsTestContainer(t, "listWithFilterA", true)
_, testContainerB := preparePsTestContainer(t, "listWithFilterB", true)
@@ -553,4 +576,54 @@ func TestContainerListWithFilter(t *testing.T) {
}
return nil
})
+
+ // filter container state without option "-a".
+ base.Cmd("ps", "--filter", "status=exited").AssertOutWithFunc(func(stdout string) error {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ if len(lines) < 2 {
+ return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
+ }
+
+ tab := tabutil.NewReader("CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES")
+ err := tab.ParseHeader(lines[0])
+ if err != nil {
+ return fmt.Errorf("failed to parse header: %v", err)
+ }
+ containerNames := map[string]struct{}{
+ testContainerC.name: {},
+ }
+ for idx, line := range lines {
+ if idx == 0 {
+ continue
+ }
+ containerName, _ := tab.ReadRow(line, "NAMES")
+ if _, ok := containerNames[containerName]; !ok {
+ return fmt.Errorf("unexpected container %s found", containerName)
+ }
+ }
+ return nil
+ })
+}
+
+func TestContainerListCheckCreatedTime(t *testing.T) {
+ base, _ := preparePsTestContainer(t, "checkCreatedTimeA", true)
+ preparePsTestContainer(t, "checkCreatedTimeB", true)
+ preparePsTestContainer(t, "checkCreatedTimeC", false)
+ preparePsTestContainer(t, "checkCreatedTimeD", false)
+
+ var createdTimes []string
+
+ base.Cmd("ps", "--format", "'{{json .CreatedAt}}'", "-a").AssertOutWithFunc(func(stdout string) error {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ if len(lines) < 4 {
+ return fmt.Errorf("expected at least 4 lines, got %d", len(lines))
+ }
+ createdTimes = append(createdTimes, lines...)
+ return nil
+ })
+
+ slices.Reverse(createdTimes)
+ if !slices.IsSorted(createdTimes) {
+ t.Errorf("expected containers in decending order")
+ }
}
diff --git a/cmd/nerdctl/container/container_list_test.go b/cmd/nerdctl/container/container_list_test.go
new file mode 100644
index 00000000000..751cfabc64c
--- /dev/null
+++ b/cmd/nerdctl/container/container_list_test.go
@@ -0,0 +1,62 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+)
+
+// https://github.com/containerd/nerdctl/issues/2598
+func TestContainerListWithFormatLabel(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+ cID := tID
+ labelK := "label-key-" + tID
+ labelV := "label-value-" + tID
+
+ base.Cmd("run", "-d",
+ "--name", cID,
+ "--label", labelK+"="+labelV,
+ testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
+ defer base.Cmd("rm", "-f", cID).AssertOK()
+ base.Cmd("ps", "-a",
+ "--filter", "label="+labelK,
+ "--format", fmt.Sprintf("{{.Label %q}}", labelK)).AssertOutExactly(labelV + "\n")
+}
+
+func TestContainerListWithJsonFormatLabel(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+ cID := tID
+ labelK := "label-key-" + tID
+ labelV := "label-value-" + tID
+
+ base.Cmd("run", "-d",
+ "--name", cID,
+ "--label", labelK+"="+labelV,
+ testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
+ defer base.Cmd("rm", "-f", cID).AssertOK()
+ base.Cmd("ps", "-a",
+ "--filter", "label="+labelK,
+ "--format", "json").AssertOutContains(fmt.Sprintf("%s=%s", labelK, labelV))
+}
diff --git a/cmd/nerdctl/container_list_windows_test.go b/cmd/nerdctl/container/container_list_windows_test.go
similarity index 97%
rename from cmd/nerdctl/container_list_windows_test.go
rename to cmd/nerdctl/container/container_list_windows_test.go
index e655a6cd1b3..39eec453684 100644
--- a/cmd/nerdctl/container_list_windows_test.go
+++ b/cmd/nerdctl/container/container_list_windows_test.go
@@ -14,24 +14,24 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/containerd/nerdctl/pkg/tabutil"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/tabutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
type psTestContainer struct {
name string
labels map[string]string
- volumes []string
network string
}
diff --git a/cmd/nerdctl/container_logs.go b/cmd/nerdctl/container/container_logs.go
similarity index 89%
rename from cmd/nerdctl/container_logs.go
rename to cmd/nerdctl/container/container_logs.go
index 7ec46f80ccc..910cbeffa03 100644
--- a/cmd/nerdctl/container_logs.go
+++ b/cmd/nerdctl/container/container_logs.go
@@ -14,19 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"strconv"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newLogsCommand() *cobra.Command {
+func NewLogsCommand() *cobra.Command {
const shortUsage = "Fetch the logs of a container. Expected to be used with 'nerdctl run -d'."
const longUsage = `Fetch the logs of a container.
@@ -37,7 +40,7 @@ The following containers are supported:
`
var logsCommand = &cobra.Command{
Use: "logs [flags] CONTAINER",
- Args: IsExactArgs(1),
+ Args: helpers.IsExactArgs(1),
Short: shortUsage,
Long: longUsage,
RunE: logsAction,
@@ -54,7 +57,7 @@ The following containers are supported:
}
func processContainerLogsOptions(cmd *cobra.Command) (types.ContainerLogsOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerLogsOptions{}, err
}
@@ -114,7 +117,7 @@ func logsAction(cmd *cobra.Command, args []string) error {
func logsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show container names (TODO: only show containers with logs)
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
// Attempts to parse the argument given to `-n/--tail` as a uint.
diff --git a/cmd/nerdctl/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go
similarity index 58%
rename from cmd/nerdctl/container_logs_test.go
rename to cmd/nerdctl/container/container_logs_test.go
index 3f78ba8e2ce..71debda52e9 100644
--- a/cmd/nerdctl/container_logs_test.go
+++ b/cmd/nerdctl/container/container_logs_test.go
@@ -14,15 +14,18 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
+ "runtime"
"strings"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestLogs(t *testing.T) {
@@ -38,9 +41,9 @@ bar`
//test since / until flag
time.Sleep(3 * time.Second)
- base.Cmd("logs", "--since", "1s", containerName).AssertNoOut(expected)
+ base.Cmd("logs", "--since", "1s", containerName).AssertOutNotContains(expected)
base.Cmd("logs", "--since", "10s", containerName).AssertOutContains(expected)
- base.Cmd("logs", "--until", "10s", containerName).AssertNoOut(expected)
+ base.Cmd("logs", "--until", "10s", containerName).AssertOutNotContains(expected)
base.Cmd("logs", "--until", "1s", containerName).AssertOutContains(expected)
// Ensure follow flag works as expected:
@@ -79,6 +82,7 @@ func TestLogsOutStreamsSeparated(t *testing.T) {
}
func TestLogsWithInheritedFlags(t *testing.T) {
+ // Seen flaky with Docker
t.Parallel()
base := testutil.NewBase(t)
for k, v := range base.Args {
@@ -92,6 +96,9 @@ func TestLogsWithInheritedFlags(t *testing.T) {
base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage,
"sh", "-euxc", "echo foo; echo bar").AssertOK()
+ // It appears this test flakes out with Docker seeing only "foo\n"
+ // Tentatively adding a pause in case this is just slow
+ time.Sleep(time.Second)
// test rootCmd alias `-n` already used in logs subcommand
base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error {
if !(stdout == "bar\n" || stdout == "") {
@@ -140,6 +147,103 @@ func TestLogsWithFailingContainer(t *testing.T) {
// AssertOutContains also asserts that the exit code of the logs command == 0,
// even when the container is failing
base.Cmd("logs", "-f", containerName).AssertOutContains("bar")
- base.Cmd("logs", "-f", containerName).AssertNoOut("baz")
+ base.Cmd("logs", "-f", containerName).AssertOutNotContains("baz")
base.Cmd("rm", "-f", containerName).AssertOK()
}
+
+func TestLogsWithForegroundContainers(t *testing.T) {
+ t.Parallel()
+ if runtime.GOOS == "windows" {
+ t.Skip("dual logging is not supported on Windows")
+ }
+ base := testutil.NewBase(t)
+ tid := testutil.Identifier(t)
+
+ // unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
+ // unbuffer(1) can be installed with `apt-get install expect`.
+ unbuffer := []string{"unbuffer"}
+
+ testCases := []struct {
+ name string
+ flags []string
+ tty bool
+ }{
+ {
+ name: "foreground",
+ flags: nil,
+ tty: false,
+ },
+ {
+ name: "interactive",
+ flags: []string{"-i"},
+ tty: false,
+ },
+ {
+ name: "PTY",
+ flags: []string{"-t"},
+ tty: true,
+ },
+ {
+ name: "interactivePTY",
+ flags: []string{"-i", "-t"},
+ tty: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ func(t *testing.T) {
+ containerName := tid + "-" + tc.name
+ var cmdArgs []string
+ defer base.Cmd("rm", "-f", containerName).Run()
+ cmdArgs = append(cmdArgs, "run", "--name", containerName)
+ cmdArgs = append(cmdArgs, tc.flags...)
+ cmdArgs = append(cmdArgs, testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar")
+
+ if tc.tty {
+ base.CmdWithHelper(unbuffer, cmdArgs...).AssertOK()
+ } else {
+ base.Cmd(cmdArgs...).AssertOK()
+ }
+
+ base.Cmd("logs", containerName).AssertOutContains("foo")
+ base.Cmd("logs", containerName).AssertOutContains("bar")
+ base.Cmd("logs", containerName).AssertOutNotContains("baz")
+ }(t)
+ }
+}
+
+func TestTailFollowRotateLogs(t *testing.T) {
+ // FIXME this is flaky by nature... 2 lines is arbitrary, 10000 ms is arbitrary, and both are some sort of educated
+ // guess that things will mostly always kinda work maybe...
+ // Furthermore, parallelizing will put pressure on the daemon which might be even slower in answering, increasing
+ // the risk of transient failure.
+ // This test needs to be rethought entirely
+ // t.Parallel()
+ if runtime.GOOS == "windows" {
+ t.Skip("tail log is not supported on Windows")
+ }
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+
+ const sampleJSONLog = `{"log":"A\n","stream":"stdout","time":"2024-04-11T12:01:09.800288974Z"}`
+ const linesPerFile = 200
+
+ defer base.Cmd("rm", "-f", containerName).Run()
+ base.Cmd("run", "-d", "--log-driver", "json-file",
+ "--log-opt", fmt.Sprintf("max-size=%d", len(sampleJSONLog)*linesPerFile),
+ "--log-opt", "max-file=10",
+ "--name", containerName, testutil.CommonImage,
+ "sh", "-euc", "while true; do echo A; usleep 100; done").AssertOK()
+
+ tailLogCmd := base.Cmd("logs", "-f", containerName)
+ tailLogCmd.Timeout = 1000 * time.Millisecond
+ logRun := tailLogCmd.Run()
+ tailLogs := strings.Split(strings.TrimSpace(logRun.Stdout()), "\n")
+ for _, line := range tailLogs {
+ if line != "" {
+ assert.Equal(t, "A", line)
+ }
+ }
+ assert.Equal(t, true, len(tailLogs) > linesPerFile, logRun.Stderr())
+}
diff --git a/cmd/nerdctl/container_pause.go b/cmd/nerdctl/container/container_pause.go
similarity index 79%
rename from cmd/nerdctl/container_pause.go
rename to cmd/nerdctl/container/container_pause.go
index 8e4f0c85f2f..338652f8d96 100644
--- a/cmd/nerdctl/container_pause.go
+++ b/cmd/nerdctl/container/container_pause.go
@@ -14,17 +14,21 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newPauseCommand() *cobra.Command {
+func NewPauseCommand() *cobra.Command {
var pauseCommand = &cobra.Command{
Use: "pause [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -38,7 +42,7 @@ func newPauseCommand() *cobra.Command {
}
func processContainerPauseOptions(cmd *cobra.Command) (types.ContainerPauseOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerPauseOptions{}, err
}
@@ -68,5 +72,5 @@ func pauseShellComplete(cmd *cobra.Command, args []string, toComplete string) ([
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st == containerd.Running
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container_port.go b/cmd/nerdctl/container/container_port.go
similarity index 84%
rename from cmd/nerdctl/container_port.go
rename to cmd/nerdctl/container/container_port.go
index e2fb467f000..1b618be020b 100644
--- a/cmd/nerdctl/container_port.go
+++ b/cmd/nerdctl/container/container_port.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"context"
@@ -22,14 +22,16 @@ import (
"strconv"
"strings"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
-func newPortCommand() *cobra.Command {
+func NewPortCommand() *cobra.Command {
var portCommand = &cobra.Command{
Use: "port [flags] CONTAINER [PRIVATE_PORT[/PROTO]]",
Args: cobra.RangeArgs(1, 2),
@@ -43,7 +45,7 @@ func newPortCommand() *cobra.Command {
}
func portAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -99,5 +101,5 @@ func portAction(cmd *cobra.Command, args []string) error {
}
func portShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
diff --git a/cmd/nerdctl/container_prune.go b/cmd/nerdctl/container/container_prune.go
similarity index 79%
rename from cmd/nerdctl/container_prune.go
rename to cmd/nerdctl/container/container_prune.go
index dc02b5a316b..79ce0d8bb56 100644
--- a/cmd/nerdctl/container_prune.go
+++ b/cmd/nerdctl/container/container_prune.go
@@ -14,16 +14,15 @@
limitations under the License.
*/
-package main
+package container
import (
- "fmt"
- "strings"
-
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
func newContainerPruneCommand() *cobra.Command {
@@ -40,7 +39,7 @@ func newContainerPruneCommand() *cobra.Command {
}
func processContainerPruneOptions(cmd *cobra.Command) (types.ContainerPruneOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerPruneOptions{}, err
}
@@ -58,15 +57,7 @@ func grantPrunePermission(cmd *cobra.Command) (bool, error) {
}
if !force {
- var confirm string
- msg := "This will remove all stopped containers."
- msg += "\nAre you sure you want to continue? [y/N] "
- fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg)
- fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm)
-
- if strings.ToLower(confirm) != "y" {
- return false, nil
- }
+ return helpers.Confirm(cmd, "WARNING! This will remove all stopped containers.")
}
return true, nil
}
diff --git a/cmd/nerdctl/container/container_prune_linux_test.go b/cmd/nerdctl/container/container_prune_linux_test.go
new file mode 100644
index 00000000000..4ff48954385
--- /dev/null
+++ b/cmd/nerdctl/container/container_prune_linux_test.go
@@ -0,0 +1,55 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestPruneContainer(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.Private
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("1"))
+ helpers.Anyhow("rm", "-f", data.Identifier("2"))
+ }
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-d", "--name", data.Identifier("1"), "-v", "/anonymous", testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("exec", data.Identifier("1"), "touch", "/anonymous/foo")
+ helpers.Ensure("create", "--name", data.Identifier("2"), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ helpers.Ensure("container", "prune", "-f")
+ helpers.Ensure("inspect", data.Identifier("1"))
+ helpers.Fail("inspect", data.Identifier("2"))
+ // https://github.com/containerd/nerdctl/issues/3134
+ helpers.Ensure("exec", data.Identifier("1"), "ls", "-lA", "/anonymous/foo")
+ helpers.Ensure("kill", data.Identifier("1"))
+ helpers.Ensure("container", "prune", "-f")
+ return helpers.Command("inspect", data.Identifier("1"))
+ }
+
+ testCase.Expected = test.Expects(1, nil, nil)
+}
diff --git a/cmd/nerdctl/container_remove.go b/cmd/nerdctl/container/container_remove.go
similarity index 80%
rename from cmd/nerdctl/container_remove.go
rename to cmd/nerdctl/container/container_remove.go
index b6c0d39d472..fd50457a179 100644
--- a/cmd/nerdctl/container_remove.go
+++ b/cmd/nerdctl/container/container_remove.go
@@ -14,16 +14,19 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newRmCommand() *cobra.Command {
+func NewRmCommand() *cobra.Command {
var rmCommand = &cobra.Command{
Use: "rm [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -33,13 +36,14 @@ func newRmCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
+ rmCommand.Aliases = []string{"remove"}
rmCommand.Flags().BoolP("force", "f", false, "Force the removal of a running|paused|unknown container (uses SIGKILL)")
rmCommand.Flags().BoolP("volumes", "v", false, "Remove volumes associated with the container")
return rmCommand
}
func rmAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -69,5 +73,5 @@ func rmAction(cmd *cobra.Command, args []string) error {
func rmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show container names
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
diff --git a/cmd/nerdctl/container/container_remove_test.go b/cmd/nerdctl/container/container_remove_test.go
new file mode 100644
index 00000000000..36c81c2c6ab
--- /dev/null
+++ b/cmd/nerdctl/container/container_remove_test.go
@@ -0,0 +1,50 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestRemoveContainer(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ containerID := data.Identifier()
+ helpers.Fail("rm", containerID)
+
+ // FIXME: should (re-)evaluate this
+ // `kill` seems to return before the container actually stops
+ helpers.Ensure("stop", containerID)
+
+ return helpers.Command("rm", containerID)
+ }
+
+ testCase.Expected = test.Expects(0, nil, nil)
+}
diff --git a/cmd/nerdctl/container_remove_windows_test.go b/cmd/nerdctl/container/container_remove_windows_test.go
similarity index 76%
rename from cmd/nerdctl/container_remove_windows_test.go
rename to cmd/nerdctl/container/container_remove_windows_test.go
index 07ed0b44dd3..c6733ca1e9b 100644
--- a/cmd/nerdctl/container_remove_windows_test.go
+++ b/cmd/nerdctl/container/container_remove_windows_test.go
@@ -14,29 +14,15 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
-)
-
-func TestRemoveProcessContainer(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- // ignore error
- base.Cmd("rm", tID, "-f").AssertOK()
- base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK()
- defer base.Cmd("rm", tID, "-f").AssertOK()
- base.Cmd("rm", tID).AssertFail()
-
- base.Cmd("kill", tID).AssertOK()
- base.Cmd("rm", tID).AssertOK()
-}
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
func TestRemoveHyperVContainer(t *testing.T) {
base := testutil.NewBase(t)
diff --git a/cmd/nerdctl/container_rename.go b/cmd/nerdctl/container/container_rename.go
similarity index 78%
rename from cmd/nerdctl/container_rename.go
rename to cmd/nerdctl/container/container_rename.go
index 2f1d482a6c1..46b48aae1ed 100644
--- a/cmd/nerdctl/container_rename.go
+++ b/cmd/nerdctl/container/container_rename.go
@@ -14,19 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newRenameCommand() *cobra.Command {
+func NewRenameCommand() *cobra.Command {
var renameCommand = &cobra.Command{
Use: "rename [flags] CONTAINER NEW_NAME",
- Args: IsExactArgs(2),
+ Args: helpers.IsExactArgs(2),
Short: "rename a container",
RunE: renameAction,
ValidArgsFunction: renameShellComplete,
@@ -37,7 +40,7 @@ func newRenameCommand() *cobra.Command {
}
func processContainerRenameOptions(cmd *cobra.Command) (types.ContainerRenameOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerRenameOptions{}, err
}
@@ -60,5 +63,5 @@ func renameAction(cmd *cobra.Command, args []string) error {
return container.Rename(ctx, client, args[0], args[1], options)
}
func renameShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
diff --git a/cmd/nerdctl/container_rename_linux_test.go b/cmd/nerdctl/container/container_rename_linux_test.go
similarity index 88%
rename from cmd/nerdctl/container_rename_linux_test.go
rename to cmd/nerdctl/container/container_rename_linux_test.go
index b082514fe1c..cc8a6733d5f 100644
--- a/cmd/nerdctl/container_rename_linux_test.go
+++ b/cmd/nerdctl/container/container_rename_linux_test.go
@@ -14,12 +14,13 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestRename(t *testing.T) {
@@ -28,7 +29,7 @@ func TestRename(t *testing.T) {
base := testutil.NewBase(t)
defer base.Cmd("rm", "-f", testContainerName).Run()
- base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", testContainerName+"_new").Run()
base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK()
@@ -44,11 +45,11 @@ func TestRenameUpdateHosts(t *testing.T) {
base := testutil.NewBase(t)
defer base.Cmd("rm", "-f", testContainerName).Run()
- base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
base.EnsureContainerStarted(testContainerName)
defer base.Cmd("rm", "-f", testContainerName+"_1").Run()
- base.Cmd("run", "-d", "--name", testContainerName+"_1", testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", testContainerName+"_1", testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
base.EnsureContainerStarted(testContainerName + "_1")
defer base.Cmd("rm", "-f", testContainerName+"_new").Run()
diff --git a/cmd/nerdctl/container_rename_windows_test.go b/cmd/nerdctl/container/container_rename_windows_test.go
similarity index 87%
rename from cmd/nerdctl/container_rename_windows_test.go
rename to cmd/nerdctl/container/container_rename_windows_test.go
index 38d78cd5473..7532b22573f 100644
--- a/cmd/nerdctl/container_rename_windows_test.go
+++ b/cmd/nerdctl/container/container_rename_windows_test.go
@@ -14,12 +14,13 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestRenameProcessContainer(t *testing.T) {
@@ -27,7 +28,7 @@ func TestRenameProcessContainer(t *testing.T) {
base := testutil.NewBase(t)
defer base.Cmd("rm", "-f", testContainerName).Run()
- base.Cmd("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "--isolation", "process", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", testContainerName+"_new").Run()
base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK()
@@ -45,7 +46,7 @@ func TestRenameHyperVContainer(t *testing.T) {
}
defer base.Cmd("rm", "-f", testContainerName).Run()
- base.Cmd("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", testContainerName+"_new").Run()
base.Cmd("rename", testContainerName, testContainerName+"_new").AssertOK()
diff --git a/cmd/nerdctl/container_restart.go b/cmd/nerdctl/container/container_restart.go
similarity index 86%
rename from cmd/nerdctl/container_restart.go
rename to cmd/nerdctl/container/container_restart.go
index ae1b8d3d1e3..b1875a77853 100644
--- a/cmd/nerdctl/container_restart.go
+++ b/cmd/nerdctl/container/container_restart.go
@@ -14,18 +14,20 @@
limitations under the License.
*/
-package main
+package container
import (
"time"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newRestartCommand() *cobra.Command {
+func NewRestartCommand() *cobra.Command {
var restartCommand = &cobra.Command{
Use: "restart [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -40,7 +42,7 @@ func newRestartCommand() *cobra.Command {
}
func processContainerRestartOptions(cmd *cobra.Command) (types.ContainerRestartOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerRestartOptions{}, err
}
diff --git a/cmd/nerdctl/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go
similarity index 65%
rename from cmd/nerdctl/container_restart_linux_test.go
rename to cmd/nerdctl/container/container_restart_linux_test.go
index 669d94d6640..3565f9f11ad 100644
--- a/cmd/nerdctl/container_restart_linux_test.go
+++ b/cmd/nerdctl/container/container_restart_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -22,8 +22,10 @@ import (
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestRestart(t *testing.T) {
@@ -51,11 +53,11 @@ func TestRestartPIDContainer(t *testing.T) {
base := testutil.NewBase(t)
baseContainerName := testutil.Identifier(t)
- base.Cmd("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", baseContainerName).Run()
sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName)
- base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--pid=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", sharedContainerName).Run()
base.Cmd("restart", baseContainerName).AssertOK()
@@ -71,12 +73,39 @@ func TestRestartPIDContainer(t *testing.T) {
assert.Equal(t, baseOutput, sharedOutput)
}
+func TestRestartIPCContainer(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ const shmSize = "32m"
+ baseContainerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", baseContainerName).Run()
+ base.Cmd("run", "-d", "--shm-size", shmSize, "--ipc", "shareable", "--name", baseContainerName, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
+
+ sharedContainerName := fmt.Sprintf("%s-shared", baseContainerName)
+ defer base.Cmd("rm", "-f", sharedContainerName).Run()
+ base.Cmd("run", "-d", "--name", sharedContainerName, fmt.Sprintf("--ipc=container:%s", baseContainerName), testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
+
+ base.Cmd("stop", baseContainerName).Run()
+ base.Cmd("stop", sharedContainerName).Run()
+
+ base.Cmd("restart", baseContainerName).AssertOK()
+ base.Cmd("restart", sharedContainerName).AssertOK()
+
+ baseShmSizeResult := base.Cmd("exec", baseContainerName, "/bin/grep", "shm", "/proc/self/mounts").Run()
+ baseOutput := strings.TrimSpace(baseShmSizeResult.Stdout())
+ sharedShmSizeResult := base.Cmd("exec", sharedContainerName, "/bin/grep", "shm", "/proc/self/mounts").Run()
+ sharedOutput := strings.TrimSpace(sharedShmSizeResult.Stdout())
+
+ assert.Equal(t, baseOutput, sharedOutput)
+}
+
func TestRestartWithTime(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
- base.Cmd("run", "-d", "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", tID, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", tID).AssertOK()
inspect := base.InspectContainer(tID)
diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container/container_run.go
similarity index 80%
rename from cmd/nerdctl/container_run.go
rename to cmd/nerdctl/container/container_run.go
index fde5e6e023f..855d1512cdf 100644
--- a/cmd/nerdctl/container_run.go
+++ b/cmd/nerdctl/container/container_run.go
@@ -14,35 +14,40 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
"fmt"
"runtime"
+ "strings"
- "github.com/containerd/console"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/consoleutil"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/logging"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/signalutil"
- "github.com/containerd/nerdctl/pkg/taskutil"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ "github.com/containerd/console"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/pkg/annotations"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/logging"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/signalutil"
+ "github.com/containerd/nerdctl/v2/pkg/taskutil"
)
const (
tiniInitBinary = "tini"
)
-func newRunCommand() *cobra.Command {
+func NewRunCommand() *cobra.Command {
shortHelp := "Run a command in a new container. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS."
longHelp := shortHelp
switch runtime.GOOS {
@@ -68,6 +73,7 @@ func newRunCommand() *cobra.Command {
setCreateFlags(runCommand)
runCommand.Flags().BoolP("detach", "d", false, "Run container in background and print container ID")
+ runCommand.Flags().StringSliceP("attach", "a", []string{}, "Attach STDIN, STDOUT, or STDERR")
return runCommand
}
@@ -78,6 +84,7 @@ func setCreateFlags(cmd *cobra.Command) {
cmd.Flags().Bool("help", false, "show help")
cmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY")
+ cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process (default true)")
cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached")
cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`)
cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -85,6 +92,7 @@ func setCreateFlags(cmd *cobra.Command) {
})
cmd.Flags().Bool("rm", false, "Automatically remove the container when it exits")
cmd.Flags().String("pull", "missing", `Pull image before running ("always"|"missing"|"never")`)
+ cmd.Flags().BoolP("quiet", "q", false, "Suppress the pull output")
cmd.RegisterFlagCompletionFunc("pull", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"always", "missing", "never"}, cobra.ShellCompDirectiveNoFileComp
})
@@ -99,18 +107,18 @@ func setCreateFlags(cmd *cobra.Command) {
// #region platform flags
cmd.Flags().String("platform", "", "Set platform (e.g. \"amd64\", \"arm64\")") // not a slice, and there is no --all-platforms
- cmd.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
// #endregion
// #region network flags
// network (net) is defined as StringSlice, not StringArray, to allow specifying "--network=cni1,cni2"
- cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|)`)
+ cmd.Flags().StringSlice("network", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`)
cmd.RegisterFlagCompletionFunc("network", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteNetworkNames(cmd, []string{})
+ return completion.NetworkNames(cmd, []string{})
})
- cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|)`)
+ cmd.Flags().StringSlice("net", []string{netutil.DefaultNetworkName}, `Connect a container to a network ("bridge"|"host"|"none"|"container:"|"ns:"|)`)
cmd.RegisterFlagCompletionFunc("net", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteNetworkNames(cmd, []string{})
+ return completion.NetworkNames(cmd, []string{})
})
// dns is defined as StringSlice, not StringArray, to allow specifying "--dns=1.1.1.1,8.8.8.8" (compatible with Podman)
cmd.Flags().StringSlice("dns", nil, "Set custom DNS servers")
@@ -120,8 +128,8 @@ func setCreateFlags(cmd *cobra.Command) {
cmd.Flags().StringSlice("dns-option", nil, "Set DNS options")
// publish is defined as StringSlice, not StringArray, to allow specifying "--publish=80:80,443:443" (compatible with Podman)
cmd.Flags().StringSliceP("publish", "p", nil, "Publish a container's port(s) to the host")
- // FIXME: not support IPV6 yet
cmd.Flags().String("ip", "", "IPv4 address to assign to the container")
+ cmd.Flags().String("ip6", "", "IPv6 address to assign to the container")
cmd.Flags().StringP("hostname", "h", "", "Container host name")
cmd.Flags().String("mac-address", "", "MAC address to assign to the container")
// #endregion
@@ -172,7 +180,12 @@ func setCreateFlags(cmd *cobra.Command) {
// #region security flags
cmd.Flags().StringArray("security-opt", []string{}, "Security options")
cmd.RegisterFlagCompletionFunc("security-opt", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return []string{"seccomp=", "seccomp=unconfined", "apparmor=", "apparmor=" + defaults.AppArmorProfileName, "apparmor=unconfined", "no-new-privileges", "privileged-without-host-devices"}, cobra.ShellCompDirectiveNoFileComp
+ return []string{
+ "seccomp=", "seccomp=" + defaults.SeccompProfileName, "seccomp=unconfined",
+ "apparmor=", "apparmor=" + defaults.AppArmorProfileName, "apparmor=unconfined",
+ "no-new-privileges",
+ "systempaths=unconfined",
+ "privileged-without-host-devices"}, cobra.ShellCompDirectiveNoFileComp
})
// cap-add and cap-drop are defined as StringSlice, not StringArray, to allow specifying "--cap-add=CAP_SYS_ADMIN,CAP_NET_ADMIN" (compatible with Podman)
cmd.Flags().StringSlice("cap-add", []string{}, "Add Linux capabilities")
@@ -180,6 +193,7 @@ func setCreateFlags(cmd *cobra.Command) {
cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities")
cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete)
cmd.Flags().Bool("privileged", false, "Give extended privileges to this container")
+ cmd.Flags().String("systemd", "false", "Allow running systemd in this container (default: false)")
// #endregion
// #region runtime flags
@@ -199,6 +213,8 @@ func setCreateFlags(cmd *cobra.Command) {
// tmpfs needs to be StringArray, not StringSlice, to prevent "/foo:size=64m,exec" from being split to {"/foo:size=64m", "exec"}
cmd.Flags().StringArray("tmpfs", nil, "Mount a tmpfs directory")
cmd.Flags().StringArray("mount", nil, "Attach a filesystem mount to the container")
+ // volumes-from needs to be StringArray, not StringSlice, to prevent "id1,id2" from being split to {"id1", "id2"} (compatible with Docker)
+ cmd.Flags().StringArray("volumes-from", nil, "Mount volumes from the specified container(s)")
// #endregion
// rootfs flags
@@ -223,8 +239,10 @@ func setCreateFlags(cmd *cobra.Command) {
cmd.Flags().String("name", "", "Assign a name to the container")
// label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"}
cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container")
- cmd.RegisterFlagCompletionFunc("label", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return labels.ShellCompletions, cobra.ShellCompDirectiveNoFileComp
+ // annotation needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"}
+ cmd.Flags().StringArray("annotation", nil, "Add an annotation to the container (passed through to the OCI runtime)")
+ cmd.RegisterFlagCompletionFunc("annotation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ return annotations.ShellCompletions, cobra.ShellCompDirectiveNoFileComp
})
// label-file is defined as StringSlice, not StringArray, to allow specifying "--env-file=FILE1,FILE2" (compatible with Podman)
@@ -269,32 +287,55 @@ func setCreateFlags(cmd *cobra.Command) {
}
-func processCreateCommandFlagsInRun(cmd *cobra.Command) (opt types.ContainerCreateOptions, err error) {
- opt, err = processContainerCreateOptions(cmd)
+func processCreateCommandFlagsInRun(cmd *cobra.Command) (types.ContainerCreateOptions, error) {
+ opt, err := processContainerCreateOptions(cmd)
if err != nil {
- return
+ return opt, err
}
opt.InRun = true
+ opt.SigProxy, err = cmd.Flags().GetBool("sig-proxy")
+ if err != nil {
+ return opt, err
+ }
opt.Interactive, err = cmd.Flags().GetBool("interactive")
if err != nil {
- return
+ return opt, err
}
opt.Detach, err = cmd.Flags().GetBool("detach")
if err != nil {
- return
+ return opt, err
}
opt.DetachKeys, err = cmd.Flags().GetString("detach-keys")
if err != nil {
- return
+ return opt, err
+ }
+ opt.Attach, err = cmd.Flags().GetStringSlice("attach")
+ if err != nil {
+ return opt, err
+ }
+
+ validAttachFlag := true
+ for i, str := range opt.Attach {
+ opt.Attach[i] = strings.ToUpper(str)
+
+ if opt.Attach[i] != "STDIN" && opt.Attach[i] != "STDOUT" && opt.Attach[i] != "STDERR" {
+ validAttachFlag = false
+ }
+ }
+ if !validAttachFlag {
+ return opt, fmt.Errorf("invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR")
}
+
return opt, nil
}
// runAction is heavily based on ctr implementation:
// https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go
func runAction(cmd *cobra.Command, args []string) error {
+ var isDetached bool
+
createOpt, err := processCreateCommandFlagsInRun(cmd)
if err != nil {
return err
@@ -310,12 +351,16 @@ func runAction(cmd *cobra.Command, args []string) error {
return errors.New("flags -d and --rm cannot be specified together")
}
+ if len(createOpt.Attach) > 0 && createOpt.Detach {
+ return errors.New("flags -d and -a cannot be specified together")
+ }
+
netFlags, err := loadNetworkFlags(cmd)
if err != nil {
return fmt.Errorf("failed to load networking flags: %s", err)
}
- netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags)
+ netManager, err := containerutil.NewNetworkingOptionsManager(createOpt.GOptions, netFlags, client)
if err != nil {
return err
}
@@ -335,25 +380,26 @@ func runAction(cmd *cobra.Command, args []string) error {
}()
id := c.ID()
- if createOpt.Rm && !createOpt.Detach {
+ if createOpt.Rm {
defer func() {
- // NOTE: OCI hooks (which are used for CNI network setup/teardown on Linux)
- // are not currently supported on Windows, so we must explicitly call
- // network setup/cleanup from the main nerdctl executable.
- if runtime.GOOS == "windows" {
- if err := netManager.CleanupNetworking(ctx, c); err != nil {
- logrus.Warnf("failed to clean up container networking: %s", err)
- }
+ if isDetached {
+ return
+ }
+ if err := netManager.CleanupNetworking(ctx, c); err != nil {
+ log.L.Warnf("failed to clean up container networking: %s", err)
}
- if err := container.RemoveContainer(ctx, c, createOpt.GOptions, true, true); err != nil {
- logrus.WithError(err).Warnf("failed to remove container %s", id)
+ if err := container.RemoveContainer(ctx, c, createOpt.GOptions, true, true, client); err != nil {
+ log.L.WithError(err).Warnf("failed to remove container %s", id)
}
}()
}
var con console.Console
if createOpt.TTY && !createOpt.Detach {
- con = console.Current()
+ con, err = consoleutil.Current()
+ if err != nil {
+ return err
+ }
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
@@ -366,8 +412,8 @@ func runAction(cmd *cobra.Command, args []string) error {
}
logURI := lab[labels.LogURI]
detachC := make(chan struct{})
- task, err := taskutil.NewTask(ctx, client, c, false, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
- con, logURI, createOpt.DetachKeys, detachC)
+ task, err := taskutil.NewTask(ctx, client, c, createOpt.Attach, createOpt.Interactive, createOpt.TTY, createOpt.Detach,
+ con, logURI, createOpt.DetachKeys, createOpt.GOptions.Namespace, detachC)
if err != nil {
return err
}
@@ -376,16 +422,18 @@ func runAction(cmd *cobra.Command, args []string) error {
}
if createOpt.Detach {
- fmt.Fprintf(createOpt.Stdout, "%s\n", id)
+ fmt.Fprintln(createOpt.Stdout, id)
return nil
}
if createOpt.TTY {
if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil {
- logrus.WithError(err).Error("console resize")
+ log.L.WithError(err).Error("console resize")
}
} else {
- sigC := signalutil.ForwardAllSignals(ctx, task)
- defer signalutil.StopCatch(sigC)
+ if createOpt.SigProxy {
+ sigC := signalutil.ForwardAllSignals(ctx, task)
+ defer signalutil.StopCatch(sigC)
+ }
}
statusC, err := task.Wait(ctx)
@@ -406,10 +454,11 @@ func runAction(cmd *cobra.Command, args []string) error {
return errors.New("got a nil IO from the task")
}
io.Wait()
+ isDetached = true
case status := <-statusC:
if createOpt.Rm {
if _, taskDeleteErr := task.Delete(ctx); taskDeleteErr != nil {
- logrus.Error(taskDeleteErr)
+ log.L.Error(taskDeleteErr)
}
}
code, _, err := status.Result()
@@ -422,3 +471,10 @@ func runAction(cmd *cobra.Command, args []string) error {
}
return nil
}
+
+func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ if len(args) == 0 {
+ return completion.ImageNames(cmd)
+ }
+ return nil, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/nerdctl/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go
similarity index 76%
rename from cmd/nerdctl/container_run_cgroup_linux_test.go
rename to cmd/nerdctl/container/container_run_cgroup_linux_test.go
index 0301ae58e66..c4b5c77de7c 100644
--- a/cmd/nerdctl/container_run_cgroup_linux_test.go
+++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go
@@ -14,21 +14,28 @@
limitations under the License.
*/
-package main
+package container
import (
"bytes"
+ "context"
"fmt"
"os"
"path/filepath"
"testing"
+ "github.com/moby/sys/userns"
+ "gotest.tools/v3/assert"
+
"github.com/containerd/cgroups/v3"
- "github.com/containerd/containerd/pkg/userns"
+ containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/continuity/testutil/loopback"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
)
func TestRunCgroupV2(t *testing.T) {
@@ -94,7 +101,7 @@ func TestRunCgroupV2(t *testing.T) {
"cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2)
base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate1", "-w", "/sys/fs/cgroup", "-d",
- testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate1").Run()
update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000",
"--memory", "42m",
@@ -113,7 +120,7 @@ func TestRunCgroupV2(t *testing.T) {
defer base.Cmd("rm", "-f", testutil.Identifier(t)+"-testUpdate2").Run()
base.Cmd("run", "--name", testutil.Identifier(t)+"-testUpdate2", "-w", "/sys/fs/cgroup", "-d",
- testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
base.EnsureContainerStarted(testutil.Identifier(t) + "-testUpdate2")
base.Cmd("update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000",
@@ -167,6 +174,53 @@ func TestRunCgroupV1(t *testing.T) {
base.Cmd("run", "--rm", "--cpu-quota", "42000", "--cpu-period", "100000", "--cpuset-mems", "0", "--memory", "42m", "--memory-reservation", "6m", "--memory-swap", "100m", "--memory-swappiness", "0", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1", testutil.AlpineImage, "cat", quota, period, cpusetMems, memoryLimit, memoryReservation, memorySwap, memorySwappiness, pidsLimit, cpuShare, cpusetCpus).AssertOutExactly(expected)
}
+// TestIssue3781 tests https://github.com/containerd/nerdctl/issues/3781
+func TestIssue3781(t *testing.T) {
+ t.Parallel()
+ testCase := nerdtest.Setup()
+ testCase.Require = test.Not(nerdtest.Docker)
+
+ base := testutil.NewBase(t)
+ info := base.Info()
+ switch info.CgroupDriver {
+ case "none", "":
+ t.Skip("test requires cgroup driver")
+ }
+ containerName := testutil.Identifier(t)
+ base.Cmd("run", "-d", "--name", containerName, testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ defer func() {
+ base.Cmd("rm", "-f", containerName)
+ }()
+ base.Cmd("update", "--cpuset-cpus", "0-1", containerName).AssertOK()
+ addr := base.ContainerdAddress()
+ client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace))
+ assert.NilError(base.T, err)
+ ctx := context.Background()
+
+ // get container id by container name.
+ var cid string
+ var args []string
+ args = append(args, containerName)
+ walker := &containerwalker.ContainerWalker{
+ Client: client,
+ OnFound: func(ctx context.Context, found containerwalker.Found) error {
+ if found.MatchCount > 1 {
+ return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ cid = found.Container.ID()
+ return nil
+ },
+ }
+ err = walker.WalkAll(ctx, args, true)
+ assert.NilError(base.T, err)
+
+ container, err := client.LoadContainer(ctx, cid)
+ assert.NilError(base.T, err)
+ spec, err := container.Spec(ctx)
+ assert.NilError(base.T, err)
+ assert.Equal(t, spec.Linux.Resources.Pids == nil, true)
+}
+
func TestRunDevice(t *testing.T) {
if os.Geteuid() != 0 || userns.RunningInUserNS() {
t.Skip("test requires the root in the initial user namespace")
@@ -197,7 +251,7 @@ func TestRunDevice(t *testing.T) {
"--name", containerName,
"--device", lo[0].Device+":r",
"--device", lo[1].Device,
- testutil.AlpineImage, "sleep", "infinity").Run()
+ testutil.AlpineImage, "sleep", nerdtest.Infinity).Run()
base.Cmd("exec", containerName, "cat", lo[0].Device).AssertOutContains(loContent[0])
base.Cmd("exec", containerName, "cat", lo[1].Device).AssertOutContains(loContent[1])
@@ -212,39 +266,46 @@ func TestRunDevice(t *testing.T) {
func TestParseDevice(t *testing.T) {
t.Parallel()
type testCase struct {
- s string
- expectedDevPath string
- expectedMode string
- err string
+ s string
+ expectedDevPath string
+ expectedContainerPath string
+ expectedMode string
+ err string
}
testCases := []testCase{
{
- s: "/dev/sda1",
- expectedDevPath: "/dev/sda1",
- expectedMode: "rwm",
+ s: "/dev/sda1",
+ expectedDevPath: "/dev/sda1",
+ expectedContainerPath: "/dev/sda1",
+ expectedMode: "rwm",
},
{
- s: "/dev/sda2:r",
- expectedDevPath: "/dev/sda2",
- expectedMode: "r",
+ s: "/dev/sda2:r",
+ expectedDevPath: "/dev/sda2",
+ expectedContainerPath: "/dev/sda2",
+ expectedMode: "r",
},
{
- s: "/dev/sda3:rw",
- expectedDevPath: "/dev/sda3",
- expectedMode: "rw",
+ s: "/dev/sda3:rw",
+ expectedDevPath: "/dev/sda3",
+ expectedContainerPath: "/dev/sda3",
+ expectedMode: "rw",
},
{
s: "sda4",
err: "not an absolute path",
},
{
- s: "/dev/sda5:/dev/sda5",
- expectedDevPath: "/dev/sda5",
- expectedMode: "rwm",
+ s: "/dev/sda5:/dev/sda5",
+ expectedDevPath: "/dev/sda5",
+ expectedContainerPath: "/dev/sda5",
+ expectedMode: "rwm",
},
{
- s: "/dev/sda6:/dev/foo6",
- err: "not supported yet",
+ s: "/dev/sda6:/dev/foo6",
+ expectedDevPath: "/dev/sda6",
+ expectedContainerPath: "/dev/foo6",
+ expectedMode: "rwm",
},
{
s: "/dev/sda7:/dev/sda7:rwmx",
@@ -254,10 +315,11 @@ func TestParseDevice(t *testing.T) {
for _, tc := range testCases {
t.Log(tc.s)
- devPath, mode, err := container.ParseDevice(tc.s)
+ devPath, containerPath, mode, err := container.ParseDevice(tc.s)
if tc.err == "" {
assert.NilError(t, err)
assert.Equal(t, tc.expectedDevPath, devPath)
+ assert.Equal(t, tc.expectedContainerPath, containerPath)
assert.Equal(t, tc.expectedMode, mode)
} else {
assert.ErrorContains(t, err, tc.err)
@@ -288,14 +350,12 @@ func TestRunCgroupParent(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
info := base.Info()
- containerName := testutil.Identifier(t)
- defer base.Cmd("rm", "-f", containerName).Run()
-
switch info.CgroupDriver {
case "none", "":
t.Skip("test requires cgroup driver")
}
+ containerName := testutil.Identifier(t)
t.Logf("Using %q cgroup driver", info.CgroupDriver)
parent := "/foobarbaz"
@@ -306,6 +366,13 @@ func TestRunCgroupParent(t *testing.T) {
parent = "foobarbaz.slice"
}
+ tearDown := func() {
+ base.Cmd("rm", "-f", containerName).Run()
+ }
+
+ tearDown()
+ t.Cleanup(tearDown)
+
// cgroup2 without host cgroup ns will just output 0::/ which doesn't help much to verify
// we got our expected path. This approach should work for both cgroup1 and 2, there will
// just be many more entries for cgroup1 as there'll be an entry per controller.
@@ -325,6 +392,9 @@ func TestRunCgroupParent(t *testing.T) {
expected := filepath.Join(parent, id)
if info.CgroupDriver == "systemd" {
expected = filepath.Join(parent, fmt.Sprintf("nerdctl-%s", id))
+ if base.Target == testutil.Docker {
+ expected = filepath.Join(parent, fmt.Sprintf("docker-%s", id))
+ }
}
base.Cmd("exec", containerName, "cat", "/proc/self/cgroup").AssertOutContains(expected)
}
@@ -346,7 +416,7 @@ func TestRunBlkioWeightCgroupV2(t *testing.T) {
containerName := testutil.Identifier(t)
defer base.Cmd("rm", "-f", containerName).AssertOK()
// when bfq io scheduler is used, the io.weight knob is exposed as io.bfq.weight
- base.Cmd("run", "--name", containerName, "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "--name", containerName, "--blkio-weight", "300", "-w", "/sys/fs/cgroup", testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 300\n")
base.Cmd("update", containerName, "--blkio-weight", "400").AssertOK()
base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 400\n")
diff --git a/cmd/nerdctl/container_run_freebsd.go b/cmd/nerdctl/container/container_run_freebsd.go
similarity index 82%
rename from cmd/nerdctl/container_run_freebsd.go
rename to cmd/nerdctl/container/container_run_freebsd.go
index e93f88cba9f..5ef9a6d94fb 100644
--- a/cmd/nerdctl/container_run_freebsd.go
+++ b/cmd/nerdctl/container/container_run_freebsd.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"github.com/spf13/cobra"
@@ -24,7 +24,3 @@ func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s
candidates := []string{}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-
-func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
diff --git a/cmd/nerdctl/container_run_gpus_test.go b/cmd/nerdctl/container/container_run_gpus_test.go
similarity index 95%
rename from cmd/nerdctl/container_run_gpus_test.go
rename to cmd/nerdctl/container/container_run_gpus_test.go
index b9961aeccb5..820acd8637e 100644
--- a/cmd/nerdctl/container_run_gpus_test.go
+++ b/cmd/nerdctl/container/container_run_gpus_test.go
@@ -14,14 +14,15 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
+
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
func TestParseGpusOptAll(t *testing.T) {
diff --git a/cmd/nerdctl/container_run_linux.go b/cmd/nerdctl/container/container_run_linux.go
similarity index 78%
rename from cmd/nerdctl/container_run_linux.go
rename to cmd/nerdctl/container/container_run_linux.go
index 6ee1eeb2f9f..a84bedddfdf 100644
--- a/cmd/nerdctl/container_run_linux.go
+++ b/cmd/nerdctl/container/container_run_linux.go
@@ -14,13 +14,14 @@
limitations under the License.
*/
-package main
+package container
import (
"strings"
- "github.com/containerd/containerd/pkg/cap"
"github.com/spf13/cobra"
+
+ "github.com/containerd/containerd/v2/pkg/cap"
)
func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -32,10 +33,3 @@ func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s
}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-
-func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- if len(args) == 0 {
- return shellCompleteImageNames(cmd)
- }
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
diff --git a/cmd/nerdctl/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go
similarity index 59%
rename from cmd/nerdctl/container_run_linux_test.go
rename to cmd/nerdctl/container/container_run_linux_test.go
index 678ea108d46..dc33702e1bd 100644
--- a/cmd/nerdctl/container_run_linux_test.go
+++ b/cmd/nerdctl/container/container_run_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"bufio"
@@ -26,23 +26,39 @@ import (
"net/http"
"os"
"path/filepath"
- "runtime"
"strconv"
"strings"
+ "syscall"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
)
func TestRunCustomRootfs(t *testing.T) {
testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
+ // FIXME: root issue is undiagnosed and this is very likely a containerd bug
+ // It appears that in certain conditions, the proxy content store info method will fail on the layer of the image
+ // Search for func (pcs *proxyContentStore) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
+ // Note that:
+ // - the problem is still here with containerd and nerdctl v2
+ // - it seems to affect images that are tagged multiple times, or that share a layer with another image
+ // - this test is not parallelized - but the fact that namespacing it solves the problem suggest that something
+ // happening in the default namespace BEFORE this test is run is SOMETIMES setting conditions that will make this fail
+ // Possible suspects would be concurrent pulls somehow effing things up w. namespaces.
+ base := testutil.NewBaseWithNamespace(t, testutil.Identifier(t))
rootfs := prepareCustomRootfs(base, testutil.AlpineImage)
+ t.Cleanup(func() {
+ base.Cmd("namespace", "remove", testutil.Identifier(t)).Run()
+ })
defer os.RemoveAll(rootfs)
base.Cmd("run", "--rm", "--rootfs", rootfs, "/bin/cat", "/proc/self/environ").AssertOutContains("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
base.Cmd("run", "--rm", "--entrypoint", "/bin/echo", "--rootfs", rootfs, "echo", "foo").AssertOutExactly("echo foo\n")
@@ -57,7 +73,7 @@ func prepareCustomRootfs(base *testutil.Base, imageName string) string {
base.Cmd("save", "-o", archiveTarPath, imageName).AssertOK()
rootfs, err := os.MkdirTemp(base.T.TempDir(), "rootfs")
assert.NilError(base.T, err)
- err = extractDockerArchive(archiveTarPath, rootfs)
+ err = helpers.ExtractDockerArchive(archiveTarPath, rootfs)
assert.NilError(base.T, err)
return rootfs
}
@@ -70,6 +86,64 @@ func TestRunShmSize(t *testing.T) {
base.Cmd("run", "--rm", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k")
}
+func TestRunShmSizeIPCShareable(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ const shmSize = "32m"
+
+ container := testutil.Identifier(t)
+ base.Cmd("run", "--rm", "--name", container, "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k")
+ defer base.Cmd("rm", "-f", container)
+}
+
+func TestRunIPCShareableRemoveMount(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ container := testutil.Identifier(t)
+
+ base.Cmd("run", "--name", container, "--ipc", "shareable", testutil.AlpineImage, "sleep", "0").AssertOK()
+ base.Cmd("rm", container).AssertOK()
+}
+
+func TestRunIPCContainerNotExists(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ container := testutil.Identifier(t)
+ result := base.Cmd("run", "--name", container, "--ipc", "container:abcd1234", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run()
+ defer base.Cmd("rm", "-f", container)
+ combined := result.Combined()
+ if !strings.Contains(strings.ToLower(combined), "no such container: abcd1234") {
+ t.Fatalf("unexpected output: %s", combined)
+ }
+}
+
+func TestRunShmSizeIPCContainer(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ const shmSize = "32m"
+ sharedContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run()
+ baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout())
+ defer base.Cmd("rm", "-f", baseContainerID).Run()
+
+ base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", baseContainerID),
+ testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k")
+}
+
+func TestRunIPCContainer(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ const shmSize = "32m"
+ victimContainerResult := base.Cmd("run", "-d", "--ipc", "shareable", "--shm-size", shmSize, testutil.AlpineImage, "sleep", nerdtest.Infinity).Run()
+ victimContainerID := strings.TrimSpace(victimContainerResult.Stdout())
+ defer base.Cmd("rm", "-f", victimContainerID).Run()
+
+ base.Cmd("run", "--rm", fmt.Sprintf("--ipc=container:%s", victimContainerID),
+ testutil.AlpineImage, "/bin/grep", "shm", "/proc/self/mounts").AssertOutContains("size=32768k")
+}
+
func TestRunPidHost(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
@@ -97,12 +171,12 @@ func TestRunPidContainer(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
- sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", "infinity").Run()
+ sharedContainerResult := base.Cmd("run", "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity).Run()
baseContainerID := strings.TrimSpace(sharedContainerResult.Stdout())
defer base.Cmd("rm", "-f", baseContainerID).Run()
base.Cmd("run", "--rm", fmt.Sprintf("--pid=container:%s", baseContainerID),
- testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep infinity")
+ testutil.AlpineImage, "ps", "ax").AssertOutContains("sleep " + nerdtest.Infinity)
}
func TestRunIpcHost(t *testing.T) {
@@ -206,7 +280,7 @@ func TestRunWithInit(t *testing.T) {
base := testutil.NewBase(t)
container := testutil.Identifier(t)
- base.Cmd("run", "-d", "--name", container, testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", container, testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", container).Run()
base.Cmd("stop", "--time=3", container).AssertOK()
@@ -216,7 +290,7 @@ func TestRunWithInit(t *testing.T) {
// Test with --init-path
container1 := container + "-1"
base.Cmd("run", "-d", "--name", container1, "--init-binary", "tini-custom",
- testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", container1).Run()
base.Cmd("stop", "--time=3", container1).AssertOK()
@@ -225,7 +299,7 @@ func TestRunWithInit(t *testing.T) {
// Test with --init
container2 := container + "-2"
base.Cmd("run", "-d", "--name", container2, "--init",
- testutil.AlpineImage, "sleep", "infinity").AssertOK()
+ testutil.AlpineImage, "sleep", nerdtest.Infinity).AssertOK()
defer base.Cmd("rm", "-f", container2).Run()
base.Cmd("stop", "--time=3", container2).AssertOK()
@@ -250,13 +324,84 @@ func TestRunTTY(t *testing.T) {
// tests pipe works
res := icmd.RunCmd(icmd.Command("unbuffer", "/bin/sh", "-c", fmt.Sprintf("%q run --rm -it %q echo hi | grep hi", base.Binary, testutil.CommonImage)))
- assert.Equal(t, 0, res.ExitCode, res.Combined())
+ assert.Equal(t, 0, res.ExitCode, res)
}
-func TestRunWithFluentdLogDriver(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("fluentd log driver is not yet implemented on Windows")
+func runSigProxy(t *testing.T, args ...string) (string, bool, bool) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ testContainerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", testContainerName).Run()
+
+ fullArgs := []string{"run"}
+ fullArgs = append(fullArgs, args...)
+ fullArgs = append(fullArgs,
+ "--name",
+ testContainerName,
+ testutil.CommonImage,
+ "sh",
+ "-c",
+ testutil.SigProxyTestScript,
+ )
+
+ result := base.Cmd(fullArgs...).Start()
+ process := result.Cmd.Process
+
+ // Waits until we reach the trap command in the shell script, then sends SIGINT.
+ time.Sleep(3 * time.Second)
+ syscall.Kill(process.Pid, syscall.SIGINT)
+
+ // Waits until SIGINT is sent and responded to, then kills process to avoid timeout
+ time.Sleep(3 * time.Second)
+ process.Kill()
+
+ sigIntRecieved := strings.Contains(result.Stdout(), testutil.SigProxyTrueOut)
+ timedOut := strings.Contains(result.Stdout(), testutil.SigProxyTimeoutMsg)
+
+ return result.Stdout(), sigIntRecieved, timedOut
+}
+
+func TestRunSigProxy(t *testing.T) {
+
+ type testCase struct {
+ name string
+ args []string
+ want bool
+ expectedOut string
+ }
+ testCases := []testCase{
+ {
+ name: "SigProxyDefault",
+ args: []string{},
+ want: true,
+ expectedOut: testutil.SigProxyTrueOut,
+ },
+ {
+ name: "SigProxyTrue",
+ args: []string{"--sig-proxy=true"},
+ want: true,
+ expectedOut: testutil.SigProxyTrueOut,
+ },
+ {
+ name: "SigProxyFalse",
+ args: []string{"--sig-proxy=false"},
+ want: false,
+ expectedOut: "",
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ stdout, sigIntRecieved, timedOut := runSigProxy(t, tc.args...)
+ errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, stdout)
+ assert.Equal(t, false, timedOut, errorMsg)
+ assert.Equal(t, tc.want, sigIntRecieved, errorMsg)
+ })
}
+}
+
+func TestRunWithFluentdLogDriver(t *testing.T) {
base := testutil.NewBase(t)
tempDirectory := t.TempDir()
err := os.Chmod(tempDirectory, 0777)
@@ -286,9 +431,6 @@ func TestRunWithFluentdLogDriver(t *testing.T) {
}
func TestRunWithFluentdLogDriverWithLogOpt(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("fluentd log driver is not yet implemented on Windows")
- }
base := testutil.NewBase(t)
tempDirectory := t.TempDir()
err := os.Chmod(tempDirectory, 0777)
@@ -358,3 +500,70 @@ func TestRunWithDetachKeys(t *testing.T) {
container := base.InspectContainer(containerName)
assert.Equal(base.T, container.State.Running, true)
}
+
+func TestRunWithTtyAndDetached(t *testing.T) {
+ base := testutil.NewBase(t)
+ imageName := testutil.CommonImage
+ withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t)
+ withTtyContainerName := "with-terminal-" + testutil.Identifier(t)
+
+ // without -t, fail
+ base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK()
+ defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK()
+ base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty")
+ withoutTtyContainer := base.InspectContainer(withoutTtyContainerName)
+ assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode)
+
+ // with -t, success
+ base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK()
+ defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK()
+ base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;")
+ withTtyContainer := base.InspectContainer(withTtyContainerName)
+ assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
+}
+
+// TestIssue3568 tests https://github.com/containerd/nerdctl/issues/3568
+func TestIssue3568(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Issue #3568 - Detaching from a container started by using --rm option causes the container to be deleted.",
+ // When detaching from a container, for a session started with 'docker attach', it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing.
+ // However, the flag is called '--detach-keys' in all cases, so nerdctl prints 'read detach keys' for all cases, and that's why this test is skipped for Docker.
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage)
+ // unbuffer(1) can be installed with `apt-get install expect`.
+ //
+ // "-p" is needed because we need unbuffer to read from stdin, and from [1]:
+ // "Normally, unbuffer does not read from stdin. This simplifies use of unbuffer in some situations.
+ // To use unbuffer in a pipeline, use the -p flag."
+ //
+ // [1] https://linux.die.net/man/1/unbuffer
+ cmd.WithWrapper("unbuffer", "-p")
+ cmd.WithStdin(testutil.NewDelayOnceReader(bytes.NewReader([]byte{1, 2}))) // https://www.physics.udel.edu/~watson/scen103/ascii.html
+ return cmd
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: test.All(
+ test.Contains("read detach keys"),
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.Contains(helpers.Capture("ps"), data.Identifier()))
+ },
+ ),
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go
similarity index 96%
rename from cmd/nerdctl/container_run_log_driver_syslog_test.go
rename to cmd/nerdctl/container/container_run_log_driver_syslog_test.go
index 678006362b9..36bf6c046d5 100644
--- a/cmd/nerdctl/container_run_log_driver_syslog_test.go
+++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go
@@ -14,21 +14,23 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"os"
"runtime"
+ "strconv"
"strings"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testca"
- "github.com/containerd/nerdctl/pkg/testutil/testsyslog"
syslog "github.com/yuchanns/srslog"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testca"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testsyslog"
)
func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]syslog.Priority, fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error) {
@@ -53,7 +55,7 @@ func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]
for rFK, rFV := range syslogFacilities {
fPriV := rFV
// test both string and number facility
- for _, fPriK := range []string{rFK, fmt.Sprintf("%d", int(fPriV)>>3)} {
+ for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} {
for fmtK, fmtValidFunc := range fmtValidFuncs {
fmtKT := "empty"
if fmtK != "" {
diff --git a/cmd/nerdctl/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go
similarity index 82%
rename from cmd/nerdctl/container_run_mount_linux_test.go
rename to cmd/nerdctl/container/container_run_mount_linux_test.go
index 352080de0be..1b3f651cef2 100644
--- a/cmd/nerdctl/container_run_mount_linux_test.go
+++ b/cmd/nerdctl/container/container_run_mount_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -23,11 +23,15 @@ import (
"strings"
"testing"
- "github.com/containerd/containerd/mount"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
mobymount "github.com/moby/sys/mount"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/containerd/v2/core/mount"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestRunVolume(t *testing.T) {
@@ -85,8 +89,23 @@ func TestRunVolume(t *testing.T) {
func TestRunAnonymousVolume(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
- base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage,
- "mountpoint", "-q", "/foo").AssertOK()
+ base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage).AssertOK()
+ base.Cmd("run", "--rm", "-v", "TestVolume2:/foo", testutil.AlpineImage).AssertOK()
+ base.Cmd("run", "--rm", "-v", "TestVolume", testutil.AlpineImage).AssertOK()
+
+ // Destination must be an absolute path not named volume
+ base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.AlpineImage).AssertFail()
+}
+
+func TestRunVolumeRelativePath(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ base.Dir = t.TempDir()
+ base.Cmd("run", "--rm", "-v", "./foo:/mnt/foo", testutil.AlpineImage).AssertOK()
+ base.Cmd("run", "--rm", "-v", "./foo", testutil.AlpineImage).AssertOK()
+
+ // Destination must be an absolute path not a relative path
+ base.Cmd("run", "--rm", "-v", "./foo:./foo", testutil.AlpineImage).AssertFail()
}
func TestRunAnonymousVolumeWithTypeMountFlag(t *testing.T) {
@@ -99,8 +118,8 @@ func TestRunAnonymousVolumeWithTypeMountFlag(t *testing.T) {
func TestRunAnonymousVolumeWithBuild(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
imageName := testutil.Identifier(t)
defer base.Cmd("rmi", imageName).Run()
@@ -108,9 +127,7 @@ func TestRunAnonymousVolumeWithBuild(t *testing.T) {
VOLUME /foo
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("run", "--rm", "-v", "/foo", testutil.AlpineImage,
@@ -120,8 +137,8 @@ VOLUME /foo
func TestRunCopyingUpInitialContentsOnVolume(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
imageName := testutil.Identifier(t)
defer base.Cmd("rmi", imageName).Run()
volName := testutil.Identifier(t) + "-vol"
@@ -132,9 +149,7 @@ RUN mkdir -p /mnt && echo hi > /mnt/initial_file
CMD ["cat", "/mnt/initial_file"]
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
@@ -149,8 +164,8 @@ CMD ["cat", "/mnt/initial_file"]
func TestRunCopyingUpInitialContentsOnDockerfileVolume(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
imageName := testutil.Identifier(t)
defer base.Cmd("rmi", imageName).Run()
volName := testutil.Identifier(t) + "-vol"
@@ -162,9 +177,7 @@ VOLUME /mnt
CMD ["cat", "/mnt/initial_file"]
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
//AnonymousVolume
@@ -185,8 +198,8 @@ CMD ["cat", "/mnt/initial_file"]
func TestRunCopyingUpInitialContentsOnVolumeShouldRetainSymlink(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
imageName := testutil.Identifier(t)
defer base.Cmd("rmi", imageName).Run()
@@ -197,9 +210,7 @@ CMD ["readlink", "/mnt/passwd"]
`, testutil.AlpineImage)
const expected = "../../../../../../../../../../../../../../../../../../etc/passwd\n"
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
@@ -210,8 +221,8 @@ CMD ["readlink", "/mnt/passwd"]
func TestRunCopyingUpInitialContentsShouldNotResetTheCopiedContents(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
tID := testutil.Identifier(t)
imageName := tID + "-img"
volumeName := tID + "-vol"
@@ -226,15 +237,13 @@ func TestRunCopyingUpInitialContentsShouldNotResetTheCopiedContents(t *testing.T
RUN echo -n "rev0" > /mnt/file
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("volume", "create", volumeName)
runContainer := func() {
- base.Cmd("run", "-d", "--name", containerName, "-v", volumeName+":/mnt", imageName, "sleep", "infinity").AssertOK()
+ base.Cmd("run", "-d", "--name", containerName, "-v", volumeName+":/mnt", imageName, "sleep", nerdtest.Infinity).AssertOK()
}
runContainer()
base.EnsureContainerStarted(containerName)
@@ -631,3 +640,82 @@ func isRootfsShareableMount() bool {
return false
}
+
+func TestRunVolumesFrom(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+ rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ roDir, err := os.MkdirTemp(t.TempDir(), "ro")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rwVolName := tID + "-rw"
+ roVolName := tID + "-ro"
+ for _, v := range []string{rwVolName, roVolName} {
+ defer base.Cmd("volume", "rm", "-f", v).Run()
+ base.Cmd("volume", "create", v).AssertOK()
+ }
+
+ fromContainerName := tID + "-from"
+ toContainerName := tID + "-to"
+ defer base.Cmd("rm", "-f", fromContainerName).AssertOK()
+ defer base.Cmd("rm", "-f", toContainerName).AssertOK()
+ base.Cmd("run",
+ "-d",
+ "--name", fromContainerName,
+ "-v", fmt.Sprintf("%s:/mnt1", rwDir),
+ "-v", fmt.Sprintf("%s:/mnt2:ro", roDir),
+ "-v", fmt.Sprintf("%s:/mnt3", rwVolName),
+ "-v", fmt.Sprintf("%s:/mnt4:ro", roVolName),
+ testutil.AlpineImage,
+ "top",
+ ).AssertOK()
+ base.Cmd("run",
+ "-d",
+ "--name", toContainerName,
+ "--volumes-from", fromContainerName,
+ testutil.AlpineImage,
+ "top",
+ ).AssertOK()
+ base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK()
+ base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail()
+ base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str3 > /mnt3/file3").AssertOK()
+ base.Cmd("exec", toContainerName, "sh", "-exc", "echo -n str4 > /mnt4/file4").AssertFail()
+ base.Cmd("rm", "-f", toContainerName).AssertOK()
+ base.Cmd("run",
+ "--rm",
+ "--volumes-from", fromContainerName,
+ testutil.AlpineImage,
+ "cat", "/mnt1/file1", "/mnt3/file3",
+ ).AssertOutExactly("str1str3")
+}
+
+func TestBindMountWhenHostFolderDoesNotExist(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t) + "-host-dir-not-found"
+ hostDir, err := os.MkdirTemp(t.TempDir(), "rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(hostDir)
+ hp := filepath.Join(hostDir, "does-not-exist")
+ base.Cmd("run", "--name", containerName, "-d", "-v", fmt.Sprintf("%s:/tmp",
+ hp), testutil.AlpineImage).AssertOK()
+ base.Cmd("rm", "-f", containerName).AssertOK()
+
+ // Host directory should get created
+ _, err = os.Stat(hp)
+ assert.NilError(t, err)
+
+ // Test for --mount
+ os.RemoveAll(hp)
+ base.Cmd("run", "--name", containerName, "-d", "--mount", fmt.Sprintf("type=bind, source=%s, target=/tmp",
+ hp), testutil.AlpineImage).AssertFail()
+ _, err = os.Stat(hp)
+ assert.ErrorIs(t, err, os.ErrNotExist)
+}
diff --git a/cmd/nerdctl/container/container_run_mount_windows_test.go b/cmd/nerdctl/container/container_run_mount_windows_test.go
new file mode 100644
index 00000000000..b0e6afde18a
--- /dev/null
+++ b/cmd/nerdctl/container/container_run_mount_windows_test.go
@@ -0,0 +1,215 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestRunMountVolume(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+ rwDir, err := os.MkdirTemp(t.TempDir(), "rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ roDir, err := os.MkdirTemp(t.TempDir(), "ro")
+ if err != nil {
+ t.Fatal(err)
+ }
+ rwVolName := tID + "-rw"
+ roVolName := tID + "-ro"
+ for _, v := range []string{rwVolName, roVolName} {
+ defer base.Cmd("volume", "rm", "-f", v).Run()
+ base.Cmd("volume", "create", v).AssertOK()
+ }
+
+ containerName := tID
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+ base.Cmd("run",
+ "-d",
+ "--name", containerName,
+ "-v", fmt.Sprintf("%s:C:/mnt1", rwDir),
+ "-v", fmt.Sprintf("%s:C:/mnt2:ro", roDir),
+ "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName),
+ "-v", fmt.Sprintf("%s:C:/mnt4:ro", roVolName),
+ testutil.CommonImage,
+ "ping localhost -t",
+ ).AssertOK()
+
+ base.Cmd("exec", containerName, "cmd", "/c", "echo -n str1 > C:/mnt1/file1").AssertOK()
+ base.Cmd("exec", containerName, "cmd", "/c", "echo -n str2 > C:/mnt2/file2").AssertFail()
+ base.Cmd("exec", containerName, "cmd", "/c", "echo -n str3 > C:/mnt3/file3").AssertOK()
+ base.Cmd("exec", containerName, "cmd", "/c", "echo -n str4 > C:/mnt4/file4").AssertFail()
+ base.Cmd("rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run",
+ "--rm",
+ "-v", fmt.Sprintf("%s:C:/mnt1", rwDir),
+ "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName),
+ testutil.CommonImage,
+ "cat", "C:/mnt1/file1", "C:/mnt3/file3",
+ ).AssertOutContainsAll("str1", "str3")
+ base.Cmd("run",
+ "--rm",
+ "-v", fmt.Sprintf("%s:C:/mnt3/mnt1", rwDir),
+ "-v", fmt.Sprintf("%s:C:/mnt3", rwVolName),
+ testutil.CommonImage,
+ "cat", "C:/mnt3/mnt1/file1", "C:/mnt3/file3",
+ ).AssertOutContainsAll("str1", "str3")
+}
+
+func TestRunMountVolumeInspect(t *testing.T) {
+ base := testutil.NewBase(t)
+ testContainer := testutil.Identifier(t)
+ testVolume := testutil.Identifier(t)
+
+ defer base.Cmd("volume", "rm", "-f", testVolume).Run()
+ base.Cmd("volume", "create", testVolume).AssertOK()
+ inspectVolume := base.InspectVolume(testVolume)
+ namedVolumeSource := inspectVolume.Mountpoint
+
+ base.Cmd(
+ "run", "-d", "--name", testContainer,
+ "-v", "C:/mnt1",
+ "-v", "C:/mnt2:C:/mnt2",
+ "-v", "\\\\.\\pipe\\containerd-containerd:\\\\.\\pipe\\containerd-containerd",
+ "-v", fmt.Sprintf("%s:C:/mnt3", testVolume),
+ testutil.CommonImage,
+ ).AssertOK()
+
+ inspect := base.InspectContainer(testContainer)
+ // convert array to map to get by key of Destination
+ actual := make(map[string]dockercompat.MountPoint)
+ for i := range inspect.Mounts {
+ actual[inspect.Mounts[i].Destination] = inspect.Mounts[i]
+ }
+
+ expected := []struct {
+ dest string
+ mountPoint dockercompat.MountPoint
+ }{
+ // anonymous volume
+ {
+ dest: "C:\\mnt1",
+ mountPoint: dockercompat.MountPoint{
+ Type: "volume",
+ Source: "", // source of anonymous volume is a generated path, so here will not check it.
+ Destination: "C:\\mnt1",
+ },
+ },
+
+ // bind
+ {
+ dest: "C:\\mnt2",
+ mountPoint: dockercompat.MountPoint{
+ Type: "bind",
+ Source: "C:\\mnt2",
+ Destination: "C:\\mnt2",
+ },
+ },
+
+ // named pipe
+ {
+ dest: "\\\\.\\pipe\\containerd-containerd",
+ mountPoint: dockercompat.MountPoint{
+ Type: "npipe",
+ Source: "\\\\.\\pipe\\containerd-containerd",
+ Destination: "\\\\.\\pipe\\containerd-containerd",
+ },
+ },
+
+ // named volume
+ {
+ dest: "C:\\mnt3",
+ mountPoint: dockercompat.MountPoint{
+ Type: "volume",
+ Name: testVolume,
+ Source: namedVolumeSource,
+ Destination: "C:\\mnt3",
+ },
+ },
+ }
+
+ for i := range expected {
+ testCase := expected[i]
+ t.Logf("test volume[dest=%q]", testCase.dest)
+
+ mountPoint, ok := actual[testCase.dest]
+ assert.Assert(base.T, ok)
+
+ assert.Equal(base.T, testCase.mountPoint.Type, mountPoint.Type)
+ assert.Equal(base.T, testCase.mountPoint.Destination, mountPoint.Destination)
+
+ if testCase.mountPoint.Source == "" {
+ // for anonymous volumes, we want to make sure that the source is not the same as the destination
+ assert.Assert(base.T, mountPoint.Source != testCase.mountPoint.Destination)
+ } else {
+ assert.Equal(base.T, testCase.mountPoint.Source, mountPoint.Source)
+ }
+
+ if testCase.mountPoint.Name != "" {
+ assert.Equal(base.T, testCase.mountPoint.Name, mountPoint.Name)
+ }
+ }
+}
+
+func TestRunMountAnonymousVolume(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ base.Cmd("run", "--rm", "-v", "TestVolume:C:/mnt", testutil.CommonImage).AssertOK()
+
+ // For docker-campatibility, Unrecognised volume spec: invalid volume specification: 'TestVolume'
+ base.Cmd("run", "--rm", "-v", "TestVolume", testutil.CommonImage).AssertFail()
+
+ // Destination must be an absolute path not named volume
+ base.Cmd("run", "--rm", "-v", "TestVolume2:TestVolumes", testutil.CommonImage).AssertFail()
+}
+
+func TestRunMountRelativePath(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ base.Cmd("run", "--rm", "-v", "./mnt:C:/mnt1", testutil.CommonImage, "cmd").AssertOK()
+
+ // Destination cannot be a relative path
+ base.Cmd("run", "--rm", "-v", "./mnt", testutil.CommonImage).AssertFail()
+ base.Cmd("run", "--rm", "-v", "./mnt:./mnt1", testutil.CommonImage, "cmd").AssertFail()
+}
+
+func TestRunMountNamedPipeVolume(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ base.Cmd("run", "--rm", "-v", `\\.\pipe\containerd-containerd`, testutil.CommonImage).AssertFail()
+}
+
+func TestRunMountVolumeSpec(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+ base.Cmd("run", "--rm", "-v", `InvalidPathC:\TestVolume:C:\Mount`, testutil.CommonImage).AssertFail()
+ base.Cmd("run", "--rm", "-v", `C:\TestVolume:C:\Mount:ro,rw:boot`, testutil.CommonImage).AssertFail()
+
+ // If -v is an empty string, it will be ignored
+ base.Cmd("run", "--rm", "-v", "", testutil.CommonImage).AssertOK()
+}
diff --git a/cmd/nerdctl/container_run_network.go b/cmd/nerdctl/container/container_run_network.go
similarity index 90%
rename from cmd/nerdctl/container_run_network.go
rename to cmd/nerdctl/container/container_run_network.go
index 8fc178c4bf7..fdf75a2e8ba 100644
--- a/cmd/nerdctl/container_run_network.go
+++ b/cmd/nerdctl/container/container_run_network.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package container
import (
"net"
- gocni "github.com/containerd/go-cni"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/portutil"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/spf13/cobra"
+
+ "github.com/containerd/go-cni"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/portutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) {
@@ -77,6 +79,13 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) {
}
netOpts.IPAddress = ipAddress
+ // --ip6=
+ ip6Address, err := cmd.Flags().GetString("ip6")
+ if err != nil {
+ return netOpts, err
+ }
+ netOpts.IP6Address = ip6Address
+
// -h/--hostname=
hostName, err := cmd.Flags().GetString("hostname")
if err != nil {
@@ -135,7 +144,7 @@ func loadNetworkFlags(cmd *cobra.Command) (types.NetworkOptions, error) {
return netOpts, err
}
portSlice = strutil.DedupeStrSlice(portSlice)
- portMappings := []gocni.PortMapping{}
+ portMappings := []cni.PortMapping{}
for _, p := range portSlice {
pm, err := portutil.ParseFlagP(p)
if err != nil {
diff --git a/cmd/nerdctl/container_run_network_base_test.go b/cmd/nerdctl/container/container_run_network_base_test.go
similarity index 85%
rename from cmd/nerdctl/container_run_network_base_test.go
rename to cmd/nerdctl/container/container_run_network_base_test.go
index d1cbfccc149..60a27be5202 100644
--- a/cmd/nerdctl/container_run_network_base_test.go
+++ b/cmd/nerdctl/container/container_run_network_base_test.go
@@ -16,19 +16,19 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"io"
"net"
- "regexp"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
)
// Tests various port mapping argument combinations by starting an nginx container and
@@ -222,28 +222,3 @@ func baseTestRunPort(t *testing.T, nginxImage string, nginxIndexHTMLSnippet stri
}
}
-
-func valuesOfMapStringString(m map[string]string) map[string]struct{} {
- res := make(map[string]struct{})
- for _, v := range m {
- res[v] = struct{}{}
- }
- return res
-}
-
-func extractHostPort(portMapping string, port string) (string, error) {
- // Regular expression to extract host port from port mapping information
- re := regexp.MustCompile(`(?P\d{1,5})/tcp ->.*?0.0.0.0:(?P\d{1,5}).*?`)
- portMappingLines := strings.Split(portMapping, "\n")
- for _, portMappingLine := range portMappingLines {
- // Find the matches
- matches := re.FindStringSubmatch(portMappingLine)
- // Check if there is a match
- if len(matches) >= 3 && matches[1] == port {
- // Extract the host port number
- hostPort := matches[2]
- return hostPort, nil
- }
- }
- return "", fmt.Errorf("could not extract host port from port mapping: %s", portMapping)
-}
diff --git a/cmd/nerdctl/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go
similarity index 54%
rename from cmd/nerdctl/container_run_network_linux_test.go
rename to cmd/nerdctl/container/container_run_network_linux_test.go
index 693f3d1db58..8d020905eb8 100644
--- a/cmd/nerdctl/container_run_network_linux_test.go
+++ b/cmd/nerdctl/container/container_run_network_linux_test.go
@@ -14,24 +14,64 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"io"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
"regexp"
"runtime"
"strings"
"testing"
+ "time"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
+ "github.com/containernetworking/plugins/pkg/ns"
+ "github.com/opencontainers/go-digest"
+ "github.com/vishvananda/netlink"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
+
+ "github.com/containerd/containerd/v2/defaults"
+ "github.com/containerd/containerd/v2/pkg/netns"
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
)
+func extractHostPort(portMapping string, port string) (string, error) {
+ // Regular expression to extract host port from port mapping information
+ re := regexp.MustCompile(`(?P\d{1,5})/tcp ->.*?0.0.0.0:(?P\d{1,5}).*?`)
+ portMappingLines := strings.Split(portMapping, "\n")
+ for _, portMappingLine := range portMappingLines {
+ // Find the matches
+ matches := re.FindStringSubmatch(portMappingLine)
+ // Check if there is a match
+ if len(matches) >= 3 && matches[1] == port {
+ // Extract the host port number
+ hostPort := matches[2]
+ return hostPort, nil
+ }
+ }
+ return "", fmt.Errorf("could not extract host port from port mapping: %s", portMapping)
+}
+
+func valuesOfMapStringString(m map[string]string) map[string]struct{} {
+ res := make(map[string]struct{})
+ for _, v := range m {
+ res[v] = struct{}{}
+ }
+ return res
+}
+
// TestRunInternetConnectivity tests Internet connectivity with `apk update`
func TestRunInternetConnectivity(t *testing.T) {
base := testutil.NewBase(t)
@@ -42,6 +82,7 @@ func TestRunInternetConnectivity(t *testing.T) {
type testCase struct {
args []string
}
+ customNetID := base.InspectNetwork(customNet).ID
testCases := []testCase{
{
args: []string{"--net", "bridge"},
@@ -49,6 +90,12 @@ func TestRunInternetConnectivity(t *testing.T) {
{
args: []string{"--net", customNet},
},
+ {
+ args: []string{"--net", customNetID},
+ },
+ {
+ args: []string{"--net", customNetID[:12]},
+ },
{
args: []string{"--net", "host"},
},
@@ -307,15 +354,50 @@ func TestRunPort(t *testing.T) {
}
func TestRunWithInvalidPortThenCleanUp(t *testing.T) {
+ testCase := nerdtest.Setup()
// docker does not set label restriction to 4096 bytes
- testutil.DockerIncompatible(t)
- t.Parallel()
- base := testutil.NewBase(t)
- containerName := testutil.Identifier(t)
- defer base.Cmd("rm", "-f", containerName).Run()
- base.Cmd("run", "--rm", "--name", containerName, "-p", "22200-22299:22200-22299", testutil.CommonImage).AssertFail()
- base.Cmd("run", "--rm", "--name", containerName, "-p", "22200-22299:22200-22299", testutil.CommonImage).AssertCombinedOutContains(errdefs.ErrInvalidArgument.Error())
- base.Cmd("run", "--rm", "--name", containerName, testutil.CommonImage).AssertOK()
+ testCase.Require = test.Not(nerdtest.Docker)
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Run a container with invalid ports, and then clean up.",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "--data-root", data.TempDir(), "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--data-root", data.TempDir(), "--rm", "--name", data.Identifier(), "-p", "22200-22299:22200-22299", testutil.CommonImage)
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errdefs.ErrInvalidArgument},
+ Output: func(stdout string, info string, t *testing.T) {
+ getAddrHash := func(addr string) string {
+ const addrHashLen = 8
+
+ d := digest.SHA256.FromString(addr)
+ h := d.Encoded()[0:addrHashLen]
+
+ return h
+ }
+
+ dataRoot := data.TempDir()
+ h := getAddrHash(defaults.DefaultAddress)
+ dataStore := filepath.Join(dataRoot, h)
+ namespace := string(helpers.Read(nerdtest.Namespace))
+ etchostsPath := filepath.Join(dataStore, "etchosts", namespace)
+
+ etchostsDirs, err := os.ReadDir(etchostsPath)
+
+ assert.NilError(t, err)
+ assert.Equal(t, len(etchostsDirs), 0)
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
}
func TestRunContainerWithStaticIP(t *testing.T) {
@@ -346,12 +428,17 @@ func TestRunContainerWithStaticIP(t *testing.T) {
useNetwork: true,
checkTheIPAddress: false,
},
- {
- ip: "10.4.0.2",
- shouldSuccess: true,
- useNetwork: false,
- checkTheIPAddress: false,
- },
+ // XXX see https://github.com/containerd/nerdctl/issues/3101
+ // docker 24 silently ignored the ip - now, docker 26 is erroring out - furthermore, this ip only makes sense
+ // in the context of nerdctl bridge network, so, this test needs rewritting either way
+ /*
+ {
+ ip: "10.4.0.2",
+ shouldSuccess: true,
+ useNetwork: false,
+ checkTheIPAddress: false,
+ },
+ */
}
tID := testutil.Identifier(t)
for i, tc := range testCases {
@@ -408,6 +495,24 @@ func TestRunDNS(t *testing.T) {
cmd.AssertOutContains("options attempts:10\n")
}
+func TestRunNetworkHostHostname(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ hostname, err := os.Hostname()
+ assert.NilError(t, err)
+ hostname = hostname + "\n"
+ base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "hostname").AssertOutExactly(hostname)
+ base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly(hostname)
+ base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "hostname").AssertOutExactly("override\n")
+ base.Cmd("run", "--rm", "--network", "host", "--hostname", "override", testutil.CommonImage, "sh", "-euxc", "echo $HOSTNAME").AssertOutExactly("override\n")
+}
+
+func TestRunNetworkHost2613(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ base.Cmd("run", "--rm", "--add-host", "foo:1.2.3.4", testutil.CommonImage, "getent", "hosts", "foo").AssertOutExactly("1.2.3.4 foo foo\n")
+}
+
func TestSharedNetworkStack(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("--network=container: only supports linux now")
@@ -427,7 +532,7 @@ func TestSharedNetworkStack(t *testing.T) {
"--name", containerNameJoin,
"--network=container:"+containerName,
testutil.CommonImage,
- "sleep", "infinity").AssertOK()
+ "sleep", nerdtest.Infinity).AssertOK()
base.Cmd("exec", containerNameJoin, "wget", "-qO-", "http://127.0.0.1:80").
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
@@ -439,48 +544,211 @@ func TestSharedNetworkStack(t *testing.T) {
AssertOutContains(testutil.NginxAlpineIndexHTMLSnippet)
}
+func TestRunContainerInExistingNetNS(t *testing.T) {
+ if rootlessutil.IsRootless() {
+ t.Skip("Can't create new netns in rootless mode")
+ }
+ testutil.DockerIncompatible(t)
+ base := testutil.NewBase(t)
+
+ netNS, err := netns.NewNetNS(t.TempDir() + "/netns")
+ assert.NilError(t, err)
+ err = netNS.Do(func(netns ns.NetNS) error {
+ loopback, err := netlink.LinkByName("lo")
+ assert.NilError(t, err)
+ err = netlink.LinkSetUp(loopback)
+ assert.NilError(t, err)
+ return nil
+ })
+ assert.NilError(t, err)
+ defer netNS.Remove()
+
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+ base.Cmd("run", "-d", "--name", containerName,
+ "--network=ns:"+netNS.GetPath(), testutil.NginxAlpineImage).AssertOK()
+ base.EnsureContainerStarted(containerName)
+ time.Sleep(3 * time.Second)
+
+ err = netNS.Do(func(netns ns.NetNS) error {
+ stdout, err := exec.Command("curl", "-s", "http://127.0.0.1:80").Output()
+ assert.NilError(t, err)
+ assert.Assert(t, strings.Contains(string(stdout), testutil.NginxAlpineIndexHTMLSnippet))
+ return nil
+ })
+ assert.NilError(t, err)
+}
+
func TestRunContainerWithMACAddress(t *testing.T) {
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
networkBridge := "testNetworkBridge" + tID
networkMACvlan := "testNetworkMACvlan" + tID
networkIPvlan := "testNetworkIPvlan" + tID
- base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
- base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
- base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
- t.Cleanup(func() {
+ tearDown := func() {
base.Cmd("network", "rm", networkBridge).Run()
base.Cmd("network", "rm", networkMACvlan).Run()
base.Cmd("network", "rm", networkIPvlan).Run()
- })
+ }
+
+ tearDown()
+ t.Cleanup(tearDown)
+
+ base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
+ base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
+ base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
+
+ defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage).
+ CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).
+ Run().Stdout()
+
+ passedMac := "we expect the generated mac on the output"
+
tests := []struct {
Network string
WantErr bool
Expect string
}{
- {"host", true, "conflicting options"},
- {"none", true, "can't open '/sys/class/net/eth0/address'"},
- {"container:whatever" + tID, true, "conflicting options"},
- {"bridge", false, ""},
- {networkBridge, false, ""},
- {networkMACvlan, false, ""},
+ {"host", false, defaultMac}, // anything but the actual address being passed
+ {"none", false, ""}, // nothing
+ {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container"
+ {"bridge", false, passedMac},
+ {networkBridge, false, passedMac},
+ {networkMACvlan, false, passedMac},
{networkIPvlan, true, "not support"},
}
- for _, test := range tests {
- macAddress, err := nettestutil.GenerateMACAddress()
- if err != nil {
- t.Errorf("failed to generate MAC address: %s", err)
- }
- if test.Expect == "" && !test.WantErr {
- test.Expect = macAddress
- }
- cmd := base.Cmd("run", "--rm", "--network", test.Network, "--mac-address", macAddress, testutil.CommonImage, "cat", "/sys/class/net/eth0/address")
- if test.WantErr {
- cmd.AssertFail()
- cmd.AssertCombinedOutContains(test.Expect)
- } else {
- cmd.AssertOK()
- cmd.AssertOutContains(test.Expect)
+
+ for i, test := range tests {
+ containerName := fmt.Sprintf("%s_%d", tID, i)
+ testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect)
+ expect := test.Expect
+ network := test.Network
+ wantErr := test.WantErr
+ t.Run(testName, func(tt *testing.T) {
+ tt.Parallel()
+
+ macAddress, err := nettestutil.GenerateMACAddress()
+ if err != nil {
+ t.Errorf("failed to generate MAC address: %s", err)
+ }
+ if expect == passedMac {
+ expect = macAddress
+ }
+
+ res := base.Cmd("run", "--rm", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage).
+ CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run()
+
+ if wantErr {
+ assert.Assert(t, res.ExitCode != 0, "Command should have failed", res)
+ assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined()))
+ } else {
+ assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res)
+ assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout()))
+ }
+ })
+
+ }
+}
+
+func TestHostsFileMounts(t *testing.T) {
+ if rootlessutil.IsRootless() {
+ if detachedNetNS, _ := rootlessutil.DetachedNetNS(); detachedNetNS != "" {
+ t.Skip("/etc/hosts is not writable")
}
}
+ base := testutil.NewBase(t)
+
+ base.Cmd("run", "--rm", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/hosts").AssertOK()
+ base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/hosts").AssertOK()
+ base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts:ro", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/hosts").AssertFail()
+ // add a line into /etc/hosts and remove it.
+ base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/hosts").AssertOK()
+ base.Cmd("run", "--rm", "-v", "/etc/hosts:/etc/hosts", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "head -n -1 /etc/hosts > temp && cat temp > /etc/hosts").AssertOK()
+
+ base.Cmd("run", "--rm", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK()
+ base.Cmd("run", "--rm", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK()
+ base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf:ro", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/resolv.conf").AssertFail()
+ // add a line into /etc/resolv.conf and remove it.
+ base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "echo >> /etc/resolv.conf").AssertOK()
+ base.Cmd("run", "--rm", "-v", "/etc/resolv.conf:/etc/resolv.conf", "--network", "host", testutil.CommonImage,
+ "sh", "-euxc", "head -n -1 /etc/resolv.conf > temp && cat temp > /etc/resolv.conf").AssertOK()
+}
+
+func TestRunContainerWithStaticIP6(t *testing.T) {
+ if rootlessutil.IsRootless() {
+ t.Skip("Static IP6 assignment is not supported rootless mode yet.")
+ }
+ networkName := "test-network"
+ networkSubnet := "2001:db8:5::/64"
+ _, subnet, err := net.ParseCIDR(networkSubnet)
+ assert.Assert(t, err == nil)
+ base := testutil.NewBaseWithIPv6Compatible(t)
+ base.Cmd("network", "create", networkName, "--subnet", networkSubnet, "--ipv6").AssertOK()
+ t.Cleanup(func() {
+ base.Cmd("network", "rm", networkName).Run()
+ })
+ testCases := []struct {
+ ip string
+ shouldSuccess bool
+ checkTheIPAddress bool
+ }{
+ {
+ ip: "",
+ shouldSuccess: true,
+ checkTheIPAddress: false,
+ },
+ {
+ ip: "2001:db8:5::6",
+ shouldSuccess: true,
+ checkTheIPAddress: true,
+ },
+ {
+ ip: "2001:db8:4::6",
+ shouldSuccess: false,
+ checkTheIPAddress: false,
+ },
+ }
+ tID := testutil.Identifier(t)
+ for i, tc := range testCases {
+ i := i
+ tc := tc
+ tcName := fmt.Sprintf("%+v", tc)
+ t.Run(tcName, func(t *testing.T) {
+ testContainerName := fmt.Sprintf("%s-%d", tID, i)
+ base := testutil.NewBaseWithIPv6Compatible(t)
+ args := []string{
+ "run", "--rm", "--name", testContainerName, "--network", networkName,
+ }
+ if tc.ip != "" {
+ args = append(args, "--ip6", tc.ip)
+ }
+ args = append(args, []string{testutil.NginxAlpineImage, "ip", "addr", "show", "dev", "eth0"}...)
+ cmd := base.Cmd(args...)
+ if !tc.shouldSuccess {
+ cmd.AssertFail()
+ return
+ }
+ cmd.AssertOutWithFunc(func(stdout string) error {
+ ip := helpers.FindIPv6(stdout)
+ if !subnet.Contains(ip) {
+ return fmt.Errorf("expected subnet %s include ip %s", subnet, ip)
+ }
+ if tc.checkTheIPAddress {
+ if ip.String() != tc.ip {
+ return fmt.Errorf("expected ip %s, got %s", tc.ip, ip)
+ }
+ }
+ return nil
+ })
+ })
+ }
}
diff --git a/cmd/nerdctl/container_run_network_windows_test.go b/cmd/nerdctl/container/container_run_network_windows_test.go
similarity index 87%
rename from cmd/nerdctl/container_run_network_windows_test.go
rename to cmd/nerdctl/container/container_run_network_windows_test.go
index 3ae5bcf2db3..53e9dd73bfc 100644
--- a/cmd/nerdctl/container_run_network_windows_test.go
+++ b/cmd/nerdctl/container/container_run_network_windows_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -23,10 +23,11 @@ import (
"testing"
"github.com/Microsoft/hcsshim"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
// TestRunInternetConnectivity tests Internet connectivity by pinging github.com.
@@ -51,9 +52,10 @@ func TestRunInternetConnectivity(t *testing.T) {
args := []string{"run", "--rm"}
args = append(args, tc.args...)
// TODO(aznashwan): smarter way to ensure internet connectivity is working.
- args = append(args, testutil.CommonImage, "ping github.com")
+ // ping doesn't seem to work on GitHub Actions ("Request timed out.")
+ args = append(args, testutil.CommonImage, "curl.exe -sSL https://github.com")
cmd := base.Cmd(args...)
- cmd.AssertOutContains("Reply from")
+ cmd.AssertOutContains("")
})
}
}
@@ -124,22 +126,22 @@ func TestHnsEndpointsExistDuringContainerLifecycle(t *testing.T) {
"tail", "-f",
)
t.Logf("Creating HNS lifecycle test container with command: %q", strings.Join(cmd.Command, " "))
- containerId := strings.TrimSpace(cmd.Run().Stdout())
- t.Logf("HNS endpoint lifecycle test container ID: %q", containerId)
+ containerID := strings.TrimSpace(cmd.Run().Stdout())
+ t.Logf("HNS endpoint lifecycle test container ID: %q", containerID)
// HNS endpoints should be allocated on container creation.
- assertHnsEndpointsExistence(t, true, containerId, testNet.Name)
+ assertHnsEndpointsExistence(t, true, containerID, testNet.Name)
// Starting and stopping the container should NOT affect/change the endpoints.
- base.Cmd("start", containerId).AssertOK()
- assertHnsEndpointsExistence(t, true, containerId, testNet.Name)
+ base.Cmd("start", containerID).AssertOK()
+ assertHnsEndpointsExistence(t, true, containerID, testNet.Name)
- base.Cmd("stop", containerId).AssertOK()
- assertHnsEndpointsExistence(t, true, containerId, testNet.Name)
+ base.Cmd("stop", containerID).AssertOK()
+ assertHnsEndpointsExistence(t, true, containerID, testNet.Name)
// Removing the container should remove the HNS endpoints.
- base.Cmd("rm", containerId).AssertOK()
- assertHnsEndpointsExistence(t, false, containerId, testNet.Name)
+ base.Cmd("rm", containerID).AssertOK()
+ assertHnsEndpointsExistence(t, false, containerID, testNet.Name)
}
// Returns a network to be used for testing.
diff --git a/cmd/nerdctl/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go
similarity index 96%
rename from cmd/nerdctl/container_run_restart_linux_test.go
rename to cmd/nerdctl/container/container_run_restart_linux_test.go
index be570699ac9..c3411c6aaeb 100644
--- a/cmd/nerdctl/container_run_restart_linux_test.go
+++ b/cmd/nerdctl/container/container_run_restart_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -24,11 +24,11 @@ import (
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
-
"gotest.tools/v3/assert"
"gotest.tools/v3/poll"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
)
func TestRunRestart(t *testing.T) {
@@ -41,7 +41,7 @@ func TestRunRestart(t *testing.T) {
}
base := testutil.NewBase(t)
if !base.DaemonIsKillable {
- t.Skip("daemon is not killable (hint: set \"-test.kill-daemon\")")
+ t.Skip("daemon is not killable (hint: set \"-test.allow-kill-daemon\")")
}
t.Log("NOTE: this test may take a while")
diff --git a/cmd/nerdctl/container_run_runtime_linux_test.go b/cmd/nerdctl/container/container_run_runtime_linux_test.go
similarity index 92%
rename from cmd/nerdctl/container_run_runtime_linux_test.go
rename to cmd/nerdctl/container/container_run_runtime_linux_test.go
index fc33c21004a..ea7473f2d20 100644
--- a/cmd/nerdctl/container_run_runtime_linux_test.go
+++ b/cmd/nerdctl/container/container_run_runtime_linux_test.go
@@ -14,12 +14,12 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestRunSysctl(t *testing.T) {
diff --git a/cmd/nerdctl/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go
similarity index 75%
rename from cmd/nerdctl/container_run_security_linux_test.go
rename to cmd/nerdctl/container/container_run_security_linux_test.go
index 49db43cbfe2..6a4cc35bb4b 100644
--- a/cmd/nerdctl/container_run_security_linux_test.go
+++ b/cmd/nerdctl/container/container_run_security_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -24,11 +24,11 @@ import (
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/apparmorutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
-
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func getCapEff(base *testutil.Base, args ...string) uint64 {
@@ -182,8 +182,8 @@ func TestRunApparmor(t *testing.T) {
attrCurrentEnforceExpected := fmt.Sprintf("%s (enforce)\n", defaultProfile)
base.Cmd("run", "--rm", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected)
base.Cmd("run", "--rm", "--security-opt", "apparmor="+defaultProfile, testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly(attrCurrentEnforceExpected)
- base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n")
- base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutExactly("unconfined\n")
+ base.Cmd("run", "--rm", "--security-opt", "apparmor=unconfined", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
+ base.Cmd("run", "--rm", "--privileged", testutil.AlpineImage, "cat", attrCurrentPath).AssertOutContains("unconfined")
}
// TestRunSeccompCapSysPtrace tests https://github.com/containerd/nerdctl/issues/976
@@ -193,6 +193,61 @@ func TestRunSeccompCapSysPtrace(t *testing.T) {
// Docker/Moby 's seccomp profile allows ptrace(2) by default, but containerd does not (yet): https://github.com/containerd/containerd/issues/6802
}
+func TestRunSystemPathsUnconfined(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ const findmnt = "`apk add -q findmnt && findmnt -R /proc && findmnt -R /sys`"
+ result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmnt).Run()
+ defaultContainerOutput := result.Combined()
+
+ var confined []string
+
+ for _, path := range []string{
+ "/proc/kcore",
+ "/proc/keys",
+ "/proc/latency_stats",
+ "/proc/sched_debug",
+ "/proc/scsi",
+ "/proc/timer_list",
+ "/proc/timer_stats",
+ "/sys/firmware",
+ "/sys/fs/selinux",
+ } {
+ // Not each distribution will support every masked path here.
+ if strings.Contains(defaultContainerOutput, path) {
+ confined = append(confined, path)
+ }
+ }
+
+ assert.Check(t, len(confined) != 0, "Default container has no confined paths to validate")
+
+ result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmnt).Run()
+ unconfinedContainerOutput := result.Combined()
+
+ for _, path := range confined {
+ assert.Assert(t, !strings.Contains(unconfinedContainerOutput, path), fmt.Sprintf("%s should not be masked when unconfined", path))
+ }
+
+ for _, path := range []string{
+ "/proc/acpi",
+ "/proc/bus",
+ "/proc/fs",
+ "/proc/irq",
+ "/proc/sysrq-trigger",
+ "/proc/sys",
+ } {
+ findmntPath := fmt.Sprintf("`apk add -q findmnt && findmnt %s`", path)
+
+ result := base.Cmd("run", "--rm", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run()
+
+ // Not each distribution will support every read-only path here.
+ if strings.Contains(result.Combined(), path) {
+ result = base.Cmd("run", "--rm", "--security-opt", "systempaths=unconfined", testutil.AlpineImage, "sh", "-euxc", findmntPath).Run()
+ assert.Assert(t, !strings.Contains(result.Combined(), "ro,"), fmt.Sprintf("%s should not be read-only when unconfined", path))
+ }
+ }
+}
+
func TestRunPrivileged(t *testing.T) {
// docker does not support --privileged-without-host-devices
testutil.DockerIncompatible(t)
@@ -224,7 +279,7 @@ func TestRunPrivileged(t *testing.T) {
res := base.Cmd("run", "--rm", "--privileged", "--security-opt", "privileged-without-host-devices", testutil.AlpineImage, "ls", devPath).Run()
// normally for not a exists file, the `ls` will return `1``.
- assert.Check(t, res.ExitCode != 0, res.Combined())
+ assert.Check(t, res.ExitCode != 0, res)
// something like `ls: /dev/dummy-zero: No such file or directory`
assert.Check(t, strings.Contains(res.Combined(), "No such file or directory"))
diff --git a/cmd/nerdctl/container/container_run_soci_linux_test.go b/cmd/nerdctl/container/container_run_soci_linux_test.go
new file mode 100644
index 00000000000..57cf0599525
--- /dev/null
+++ b/cmd/nerdctl/container/container_run_soci_linux_test.go
@@ -0,0 +1,76 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "os/exec"
+ "strings"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestRunSoci(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ tests := []struct {
+ name string
+ image string
+ remoteSnapshotsExpectedCount int
+ }{
+ {
+ name: "Run with SOCI",
+ image: testutil.FfmpegSociImage,
+ remoteSnapshotsExpectedCount: 11,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ base := testutil.NewBase(t)
+ helpers.RequiresSoci(base)
+
+ //counting initial snapshot mounts
+ initialMounts, err := exec.Command("mount").Output()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge")
+
+ runOutput := base.Cmd("--snapshotter=soci", "run", "--rm", testutil.FfmpegSociImage).Out()
+ base.T.Logf("run output: %s", runOutput)
+
+ actualMounts, err := exec.Command("mount").Output()
+ if err != nil {
+ t.Fatal(err)
+ }
+ remoteSnapshotsActualCount := strings.Count(string(actualMounts), "fuse.rawBridge")
+ base.T.Logf("number of actual mounts: %v", remoteSnapshotsActualCount)
+
+ rmiOutput := base.Cmd("rmi", testutil.FfmpegSociImage).Out()
+ base.T.Logf("rmi output: %s", rmiOutput)
+
+ base.T.Logf("number of expected mounts: %v", tt.remoteSnapshotsExpectedCount)
+
+ if tt.remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) {
+ t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d",
+ tt.remoteSnapshotsExpectedCount, remoteSnapshotsActualCount-remoteSnapshotsInitialCount)
+ }
+ })
+ }
+}
diff --git a/cmd/nerdctl/container_run_stargz_linux_test.go b/cmd/nerdctl/container/container_run_stargz_linux_test.go
similarity index 77%
rename from cmd/nerdctl/container_run_stargz_linux_test.go
rename to cmd/nerdctl/container/container_run_stargz_linux_test.go
index 0e1c8266b3d..70e3b0c7d55 100644
--- a/cmd/nerdctl/container_run_stargz_linux_test.go
+++ b/cmd/nerdctl/container/container_run_stargz_linux_test.go
@@ -14,28 +14,24 @@
limitations under the License.
*/
-package main
+package container
import (
+ "runtime"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestRunStargz(t *testing.T) {
testutil.DockerIncompatible(t)
+ if runtime.GOARCH != "amd64" {
+ t.Skip("skipping test as FedoraESGZImage is amd64 only")
+ }
+
base := testutil.NewBase(t)
- requiresStargz(base)
+ helpers.RequiresStargz(base)
// if stargz snapshotter is functional, "/.stargz-snapshotter" appears
base.Cmd("--snapshotter=stargz", "run", "--rm", testutil.FedoraESGZImage, "ls", "/.stargz-snapshotter").AssertOK()
}
-
-func requiresStargz(base *testutil.Base) {
- info := base.Info()
- for _, p := range info.Plugins.Storage {
- if p == "stargz" {
- return
- }
- }
- base.T.Skip("test requires stargz")
-}
diff --git a/cmd/nerdctl/container/container_run_systemd_linux_test.go b/cmd/nerdctl/container/container_run_systemd_linux_test.go
new file mode 100644
index 00000000000..065e450873c
--- /dev/null
+++ b/cmd/nerdctl/container/container_run_systemd_linux_test.go
@@ -0,0 +1,114 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestRunWithSystemdAlways(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "--name", containerName, "--systemd=always", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(rw,")
+
+ base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
+
+}
+
+func TestRunWithSystemdTrueEnabled(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "-d", "--name", containerName, "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK()
+
+ base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
+
+ base.Cmd("exec", containerName, "sh", "-c", "--", `tries=0
+until systemctl is-system-running >/dev/null 2>&1; do
+ >&2 printf "Waiting for systemd to come up...\n"
+ sleep 1s
+ tries=$(( tries + 1))
+ [ $tries -lt 10 ] || {
+ >&2 printf "systemd failed to come up in a reasonable amount of time\n"
+ exit 1
+ }
+done
+systemctl list-jobs`).AssertOutContains("jobs")
+}
+
+func TestRunWithSystemdTrueDisabled(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "--name", containerName, "--systemd=true", "--entrypoint=/bin/bash", testutil.SystemdImage, "-c", "systemctl list-jobs || true").AssertCombinedOutContains("System has not been booted with systemd as init system")
+}
+
+func TestRunWithSystemdFalse(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "--name", containerName, "--systemd=false", "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,")
+
+ base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM")
+}
+
+func TestRunWithNoSystemd(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "--name", containerName, "--entrypoint=/bin/bash", testutil.UbuntuImage, "-c", "mount | grep cgroup").AssertOutContains("(ro,")
+
+ base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGTERM")
+}
+
+func TestRunWithSystemdPrivilegedError(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ base.Cmd("run", "--privileged", "--rm", "--systemd=always", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertCombinedOutContains("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")
+}
+
+func TestRunWithSystemdPrivilegedSuccess(t *testing.T) {
+ testutil.DockerIncompatible(t)
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+ defer base.Cmd("container", "rm", "-f", containerName).AssertOK()
+
+ base.Cmd("run", "-d", "--name", containerName, "--privileged", "--security-opt", "privileged-without-host-devices", "--systemd=true", "--entrypoint=/sbin/init", testutil.SystemdImage).AssertOK()
+
+ base.Cmd("inspect", "--format", "{{json .Config.Labels}}", containerName).AssertOutContains("SIGRTMIN+3")
+}
diff --git a/cmd/nerdctl/container_run_test.go b/cmd/nerdctl/container/container_run_test.go
similarity index 61%
rename from cmd/nerdctl/container_run_test.go
rename to cmd/nerdctl/container/container_run_test.go
index 1bf584a2be3..9a22aded3b0 100644
--- a/cmd/nerdctl/container_run_test.go
+++ b/cmd/nerdctl/container/container_run_test.go
@@ -14,30 +14,36 @@
limitations under the License.
*/
-package main
+package container
import (
+ "bufio"
+ "bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
+ "regexp"
"runtime"
"strings"
"testing"
"time"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
"gotest.tools/v3/poll"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
)
func TestRunEntrypointWithBuild(t *testing.T) {
t.Parallel()
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
imageName := testutil.Identifier(t)
defer base.Cmd("rmi", imageName).Run()
@@ -46,9 +52,7 @@ ENTRYPOINT ["echo", "foo"]
CMD ["echo", "bar"]
`, testutil.CommonImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
base.Cmd("run", "--rm", imageName).AssertOutExactly("foo echo bar\n")
@@ -144,7 +148,7 @@ func TestRunCIDFile(t *testing.T) {
func TestRunEnvFile(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
- base.Env = append(os.Environ(), "HOST_ENV=ENV-IN-HOST")
+ base.Env = append(base.Env, "HOST_ENV=ENV-IN-HOST")
tID := testutil.Identifier(t)
file1, err := os.CreateTemp("", tID)
@@ -171,7 +175,7 @@ func TestRunEnvFile(t *testing.T) {
func TestRunEnv(t *testing.T) {
t.Parallel()
base := testutil.NewBase(t)
- base.Env = append(os.Environ(), "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
+ base.Env = append(base.Env, "CORGE=corge-value-in-host", "GARPLY=garply-value-in-host")
base.Cmd("run", "--rm",
"--env", "FOO=foo1,foo2",
"--env", "BAR=bar1 bar2",
@@ -216,6 +220,19 @@ func TestRunEnv(t *testing.T) {
return nil
})
}
+func TestRunHostnameEnv(t *testing.T) {
+ t.Parallel()
+ base := testutil.NewBase(t)
+
+ base.Cmd("run", "-i", "--rm", testutil.CommonImage).
+ CmdOption(testutil.WithStdin(strings.NewReader(`[[ "HOSTNAME=$(hostname)" == "$(env | grep HOSTNAME)" ]]`))).
+ AssertOK()
+
+ if runtime.GOOS == "windows" {
+ t.Skip("run --hostname not implemented on Windows yet")
+ }
+ base.Cmd("run", "--rm", "--hostname", "foobar", testutil.CommonImage, "env").AssertOutContains("HOSTNAME=foobar")
+}
func TestRunStdin(t *testing.T) {
t.Parallel()
@@ -312,19 +329,44 @@ func TestRunWithJournaldLogDriver(t *testing.T) {
time.Sleep(3 * time.Second)
journalctl, err := exec.LookPath("journalctl")
assert.NilError(t, err)
+
inspectedContainer := base.InspectContainer(containerName)
- found := 0
- check := func(log poll.LogT) poll.Result {
- res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12])))
- assert.Equal(t, 0, res.ExitCode, res.Combined())
- if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
- found = 1
- return poll.Success()
- }
- return poll.Continue("reading from journald is not yet finished")
+
+ type testCase struct {
+ name string
+ filter string
+ }
+ testCases := []testCase{
+ {
+ name: "filter journald logs using SYSLOG_IDENTIFIER field",
+ filter: fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12]),
+ },
+ {
+ name: "filter journald logs using CONTAINER_NAME field",
+ filter: fmt.Sprintf("CONTAINER_NAME=%s", containerName),
+ },
+ {
+ name: "filter journald logs using IMAGE_NAME field",
+ filter: fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage),
+ },
+ }
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ found := 0
+ check := func(log poll.LogT) poll.Result {
+ res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", tc.filter))
+ assert.Equal(t, 0, res.ExitCode, res)
+ if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
+ found = 1
+ return poll.Success()
+ }
+ return poll.Continue("reading from journald is not yet finished")
+ }
+ poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
+ assert.Equal(t, 1, found)
+ })
}
- poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second))
- assert.Equal(t, 1, found)
}
func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) {
@@ -345,7 +387,7 @@ func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) {
found := 0
check := func(log poll.LogT) poll.Result {
res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID)))
- assert.Equal(t, 0, res.ExitCode, res.Combined())
+ assert.Equal(t, 0, res.ExitCode, res)
if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") {
found = 1
return poll.Success()
@@ -357,6 +399,7 @@ func TestRunWithJournaldLogDriverAndLogOpt(t *testing.T) {
}
func TestRunWithLogBinary(t *testing.T) {
+ testutil.RequiresBuild(t)
if runtime.GOOS == "windows" {
t.Skip("buildkit is not enabled on windows, this feature may work on windows.")
}
@@ -366,8 +409,8 @@ func TestRunWithLogBinary(t *testing.T) {
imageName := testutil.Identifier(t) + "-image"
containerName := testutil.Identifier(t)
- const dockerfile = `
-FROM golang:latest as builder
+ var dockerfile = `
+FROM ` + testutil.GolangImage + ` as builder
WORKDIR /go/src/
RUN mkdir -p logger
WORKDIR /go/src/logger
@@ -383,7 +426,7 @@ RUN echo '\
"path/filepath" \n\
"sync" \n\
\n\
- "github.com/containerd/containerd/runtime/v2/logging"\n\
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"\n\
)\n\
func main() {\n\
@@ -424,9 +467,7 @@ FROM scratch
COPY --from=builder /go/src/logger/logger /
`
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
tmpDir := t.TempDir()
base.Cmd("build", buildCtx, "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", tmpDir)).AssertOK()
defer base.Cmd("image", "rm", "-f", imageName).AssertOK()
@@ -444,26 +485,230 @@ COPY --from=builder /go/src/logger/logger /
assert.Check(t, strings.Contains(log, "bar"))
}
-func TestRunWithTtyAndDetached(t *testing.T) {
+// history: There was a bug that the --add-host items disappear when the another container created.
+// This case ensures that it's doesn't happen.
+// (https://github.com/containerd/nerdctl/issues/2560)
+func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) {
if runtime.GOOS == "windows" {
- t.Skip("json-file log driver is not yet implemented on Windows")
+ t.Skip("ocihook is not yet supported on Windows")
}
base := testutil.NewBase(t)
- imageName := testutil.CommonImage
- withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t)
- withTtyContainerName := "with-terminal-" + testutil.Identifier(t)
-
- // without -t, fail
- base.Cmd("run", "-d", "--name", withoutTtyContainerName, imageName, "stty").AssertOK()
- defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK()
- base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty")
- withoutTtyContainer := base.InspectContainer(withoutTtyContainerName)
- assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode)
-
- // with -t, success
- base.Cmd("run", "-d", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK()
- defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK()
- base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;")
- withTtyContainer := base.InspectContainer(withTtyContainerName)
- assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
+
+ containerName := testutil.Identifier(t)
+ hostMapping := "test-add-host:10.0.0.1"
+ base.Cmd("run", "-d", "--add-host", hostMapping, "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK()
+ defer base.Cmd("container", "rm", "-f", containerName).Run()
+
+ checkEtcHosts := func(stdout string) error {
+ matcher, err := regexp.Compile(`^10.0.0.1\s+test-add-host$`)
+ if err != nil {
+ return err
+ }
+ var found bool
+ sc := bufio.NewScanner(bytes.NewBufferString(stdout))
+ for sc.Scan() {
+ if matcher.Match(sc.Bytes()) {
+ found = true
+ }
+ }
+ if !found {
+ return fmt.Errorf("host not found")
+ }
+ return nil
+ }
+ base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts)
+
+ // run another container
+ base.Cmd("run", "--rm", testutil.CommonImage).AssertOK()
+
+ base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts)
+}
+
+// https://github.com/containerd/nerdctl/issues/2726
+func TestRunRmTime(t *testing.T) {
+ base := testutil.NewBase(t)
+ base.Cmd("pull", testutil.CommonImage)
+ t0 := time.Now()
+ base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK()
+ t1 := time.Now()
+ took := t1.Sub(t0)
+ const deadline = 3 * time.Second
+ if took > deadline {
+ t.Fatalf("expected to have completed in %v, took %v", deadline, took)
+ }
+}
+
+func runAttachStdin(t *testing.T, testStr string, args []string) string {
+ if runtime.GOOS == "windows" {
+ t.Skip("run attach test is not yet implemented on Windows")
+ }
+
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+
+ opts := []func(*testutil.Cmd){
+ testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")),
+ }
+
+ fullArgs := []string{"run", "--rm", "-i"}
+ fullArgs = append(fullArgs, args...)
+ fullArgs = append(fullArgs,
+ "--name",
+ containerName,
+ testutil.CommonImage,
+ )
+
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+ result := base.Cmd(fullArgs...).CmdOption(opts...).Run()
+
+ return result.Combined()
+}
+
+func runAttach(t *testing.T, testStr string, args []string) string {
+ if runtime.GOOS == "windows" {
+ t.Skip("run attach test is not yet implemented on Windows")
+ }
+
+ t.Parallel()
+ base := testutil.NewBase(t)
+ containerName := testutil.Identifier(t)
+
+ fullArgs := []string{"run"}
+ fullArgs = append(fullArgs, args...)
+ fullArgs = append(fullArgs,
+ "--name",
+ containerName,
+ testutil.CommonImage,
+ "sh",
+ "-euxc",
+ "echo "+testStr,
+ )
+
+ defer base.Cmd("rm", "-f", containerName).AssertOK()
+ result := base.Cmd(fullArgs...).Run()
+
+ return result.Combined()
+}
+
+func TestRunAttachFlag(t *testing.T) {
+
+ type testCase struct {
+ name string
+ args []string
+ testFunc func(t *testing.T, testStr string, args []string) string
+ testStr string
+ expectedOut string
+ dockerOut string
+ }
+ testCases := []testCase{
+ {
+ name: "AttachFlagStdin",
+ args: []string{"-a", "STDIN", "-a", "STDOUT"},
+ testFunc: runAttachStdin,
+ testStr: "test-run-stdio",
+ expectedOut: "test-run-stdio",
+ dockerOut: "test-run-stdio",
+ },
+ {
+ name: "AttachFlagStdOut",
+ args: []string{"-a", "STDOUT"},
+ testFunc: runAttach,
+ testStr: "foo",
+ expectedOut: "foo",
+ dockerOut: "foo",
+ },
+ {
+ name: "AttachFlagMixedValue",
+ args: []string{"-a", "STDIN", "-a", "invalid-value"},
+ testFunc: runAttach,
+ testStr: "foo",
+ expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
+ dockerOut: "valid streams are STDIN, STDOUT and STDERR",
+ },
+ {
+ name: "AttachFlagInvalidValue",
+ args: []string{"-a", "invalid-stream"},
+ testFunc: runAttach,
+ testStr: "foo",
+ expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR",
+ dockerOut: "valid streams are STDIN, STDOUT and STDERR",
+ },
+ {
+ name: "AttachFlagCaseInsensitive",
+ args: []string{"-a", "stdin", "-a", "stdout"},
+ testFunc: runAttachStdin,
+ testStr: "test-run-stdio",
+ expectedOut: "test-run-stdio",
+ dockerOut: "test-run-stdio",
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ actualOut := tc.testFunc(t, tc.testStr, tc.args)
+ errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut)
+ if testutil.GetTarget() == testutil.Docker {
+ assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg)
+ } else {
+ assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg)
+ }
+ })
+ }
+}
+
+func TestRunQuiet(t *testing.T) {
+ base := testutil.NewBase(t)
+
+ teardown := func() {
+ base.Cmd("rmi", "-f", testutil.CommonImage).Run()
+ }
+ defer teardown()
+ teardown()
+
+ sentinel := "test run quiet"
+ result := base.Cmd("run", "--rm", "--quiet", testutil.CommonImage, fmt.Sprintf(`echo "%s"`, sentinel)).Run()
+ assert.Assert(t, strings.Contains(result.Combined(), sentinel))
+
+ wasQuiet := func(output, sentinel string) bool {
+ return !strings.Contains(output, sentinel)
+ }
+
+ // Docker and nerdctl image pulls are not 1:1.
+ if testutil.GetTarget() == testutil.Docker {
+ sentinel = "Pull complete"
+ } else {
+ sentinel = "resolved"
+ }
+
+ assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel)
+}
+
+func TestRunFromOCIArchive(t *testing.T) {
+ testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
+
+ // Docker does not support running container images from OCI archive.
+ testutil.DockerIncompatible(t)
+
+ base := testutil.NewBase(t)
+ imageName := testutil.Identifier(t)
+
+ teardown := func() {
+ base.Cmd("rmi", "-f", imageName).Run()
+ }
+ defer teardown()
+ teardown()
+
+ const sentinel = "test-nerdctl-run-from-oci-archive"
+ dockerfile := fmt.Sprintf(`FROM %s
+ CMD ["echo", "%s"]`, testutil.CommonImage, sentinel)
+
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
+ tag := fmt.Sprintf("%s:latest", imageName)
+ tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName)
+
+ base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK()
+ base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(fmt.Sprintf("Loaded image: %s", tag), sentinel)
}
diff --git a/cmd/nerdctl/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go
similarity index 98%
rename from cmd/nerdctl/container_run_user_linux_test.go
rename to cmd/nerdctl/container/container_run_user_linux_test.go
index 23cdee352c6..73d1753a867 100644
--- a/cmd/nerdctl/container_run_user_linux_test.go
+++ b/cmd/nerdctl/container/container_run_user_linux_test.go
@@ -14,12 +14,12 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestRunUserGID(t *testing.T) {
diff --git a/cmd/nerdctl/container_run_user_windows_test.go b/cmd/nerdctl/container/container_run_user_windows_test.go
similarity index 94%
rename from cmd/nerdctl/container_run_user_windows_test.go
rename to cmd/nerdctl/container/container_run_user_windows_test.go
index 27cfd0697af..e92dc595598 100644
--- a/cmd/nerdctl/container_run_user_windows_test.go
+++ b/cmd/nerdctl/container/container_run_user_windows_test.go
@@ -14,12 +14,12 @@
limitations under the License.
*/
-package main
+package container
import (
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestRunUserName(t *testing.T) {
diff --git a/cmd/nerdctl/container_run_verify_linux_test.go b/cmd/nerdctl/container/container_run_verify_linux_test.go
similarity index 62%
rename from cmd/nerdctl/container_run_verify_linux_test.go
rename to cmd/nerdctl/container/container_run_verify_linux_test.go
index ad4bef7d802..7d12342cbb3 100644
--- a/cmd/nerdctl/container_run_verify_linux_test.go
+++ b/cmd/nerdctl/container/container_run_verify_linux_test.go
@@ -14,46 +14,44 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
- "os"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "gotest.tools/v3/assert"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
)
func TestRunVerifyCosign(t *testing.T) {
testutil.RequireExecutable(t, "cosign")
testutil.DockerIncompatible(t)
testutil.RequiresBuild(t)
- t.Setenv("COSIGN_PASSWORD", "1")
- keyPair := newCosignKeyPair(t, "cosign-key-pair")
- defer keyPair.cleanup()
+ testutil.RegisterBuildCacheCleanup(t)
+ t.Parallel()
+
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- localhostIP := "127.0.0.1"
- t.Logf("localhost IP=%q", localhostIP)
- testImageRef := fmt.Sprintf("%s:%d/%s",
- localhostIP, reg.ListenPort, tID)
- t.Logf("testImageRef=%q", testImageRef)
+ base.Env = append(base.Env, "COSIGN_PASSWORD=1")
+ keyPair := helpers.NewCosignKeyPair(t, "cosign-key-pair", "1")
+ reg := testregistry.NewWithNoAuth(base, 0, false)
+ t.Cleanup(func() {
+ keyPair.Cleanup()
+ reg.Cleanup(nil)
+ })
+
+ tID := testutil.Identifier(t)
+ testImageRef := fmt.Sprintf("127.0.0.1:%d/%s", reg.Port, tID)
dockerfile := fmt.Sprintf(`FROM %s
CMD ["echo", "nerdctl-build-test-string"]
`, testutil.CommonImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
- base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK()
- base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key="+keyPair.publicKey, testImageRef).AssertOK()
+ base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.PrivateKey).AssertOK()
+ base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, testImageRef).AssertOK()
base.Cmd("run", "--rm", "--verify=cosign", "--cosign-key=dummy", testImageRef).AssertFail()
}
diff --git a/cmd/nerdctl/container_run_windows.go b/cmd/nerdctl/container/container_run_windows.go
similarity index 82%
rename from cmd/nerdctl/container_run_windows.go
rename to cmd/nerdctl/container/container_run_windows.go
index e93f88cba9f..5ef9a6d94fb 100644
--- a/cmd/nerdctl/container_run_windows.go
+++ b/cmd/nerdctl/container/container_run_windows.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"github.com/spf13/cobra"
@@ -24,7 +24,3 @@ func capShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s
candidates := []string{}
return candidates, cobra.ShellCompDirectiveNoFileComp
}
-
-func runShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
diff --git a/cmd/nerdctl/container_run_windows_test.go b/cmd/nerdctl/container/container_run_windows_test.go
similarity index 75%
rename from cmd/nerdctl/container_run_windows_test.go
rename to cmd/nerdctl/container/container_run_windows_test.go
index 74bc189925e..843a6eac702 100644
--- a/cmd/nerdctl/container_run_windows_test.go
+++ b/cmd/nerdctl/container/container_run_windows_test.go
@@ -14,15 +14,19 @@
limitations under the License.
*/
-package main
+package container
import (
+ "bytes"
"os/exec"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
)
func TestRunHostProcessContainer(t *testing.T) {
@@ -32,6 +36,7 @@ func TestRunHostProcessContainer(t *testing.T) {
if err != nil {
t.Fatalf("unable to get hostname: %s", err)
}
+ hostname = bytes.TrimSpace(hostname)
base.Cmd("run", "--rm", "--isolation=host", testutil.WindowsNano, "hostname").AssertOutContains(string(hostname))
output := base.Cmd("run", "--rm", "--isolation=host", testutil.WindowsNano, "whoami").Out()
@@ -114,3 +119,29 @@ func TestRunProcessContainerWithDevice(t *testing.T) {
"cmd", "/S", "/C", "dir C:\\Windows\\System32\\HostDriverStore",
).AssertOutContains("FileRepository")
}
+
+func TestRunWithTtyAndDetached(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // This test is currently disabled, as it is failing most of the time.
+ testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3437")
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ // with -t, success, the container should run with tty support.
+ helpers.Ensure("run", "-d", "-t", "--name", data.Identifier("with-terminal"), testutil.CommonImage, "cmd", "/c", "echo", "Hello, World with TTY!")
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("container", "rm", "-f", data.Identifier("with-terminal"))
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ withTtyContainer := nerdtest.InspectContainer(helpers, data.Identifier("with-terminal"))
+ assert.Equal(helpers.T(), 0, withTtyContainer.State.ExitCode)
+ return helpers.Command("logs", data.Identifier("with-terminal"))
+ }
+
+ testCase.Expected = test.Expects(0, nil, test.Contains("Hello, World with TTY!"))
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_start.go b/cmd/nerdctl/container/container_start.go
similarity index 81%
rename from cmd/nerdctl/container_start.go
rename to cmd/nerdctl/container/container_start.go
index 8596adc3bea..1bf361b8e6a 100644
--- a/cmd/nerdctl/container_start.go
+++ b/cmd/nerdctl/container/container_start.go
@@ -14,18 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/consoleutil"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
)
-func newStartCommand() *cobra.Command {
+func NewStartCommand() *cobra.Command {
var startCommand = &cobra.Command{
Use: "start [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -44,7 +48,7 @@ func newStartCommand() *cobra.Command {
}
func processContainerStartOptions(cmd *cobra.Command) (types.ContainerStartOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerStartOptions{}, err
}
@@ -84,5 +88,5 @@ func startShellComplete(cmd *cobra.Command, args []string, toComplete string) ([
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st != containerd.Running && st != containerd.Unknown
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go
similarity index 82%
rename from cmd/nerdctl/container_start_linux_test.go
rename to cmd/nerdctl/container/container_start_linux_test.go
index 13ddf477626..4fe6f2d249c 100644
--- a/cmd/nerdctl/container_start_linux_test.go
+++ b/cmd/nerdctl/container/container_start_linux_test.go
@@ -14,27 +14,22 @@
limitations under the License.
*/
-package main
+package container
import (
"bytes"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestStartDetachKeys(t *testing.T) {
t.Parallel()
- if testutil.GetTarget() == testutil.Docker {
- t.Skip("When detaching from a container, for a session started with 'docker attach'" +
- ", it prints 'read escape sequence', but for one started with 'docker (run|start)', it prints nothing." +
- " However, the flag is called '--detach-keys' in all cases" +
- ", so nerdctl prints 'read detach keys' for all cases" +
- ", and that's why this test is skipped for Docker.")
- }
+ skipAttachForDocker(t)
base := testutil.NewBase(t)
containerName := testutil.Identifier(t)
diff --git a/cmd/nerdctl/container_start_test.go b/cmd/nerdctl/container/container_start_test.go
similarity index 95%
rename from cmd/nerdctl/container_start_test.go
rename to cmd/nerdctl/container/container_start_test.go
index b9721b18241..60369433d24 100644
--- a/cmd/nerdctl/container_start_test.go
+++ b/cmd/nerdctl/container/container_start_test.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package container
import (
"runtime"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestStart(t *testing.T) {
diff --git a/cmd/nerdctl/container/container_stats.go b/cmd/nerdctl/container/container_stats.go
new file mode 100644
index 00000000000..10b927a750b
--- /dev/null
+++ b/cmd/nerdctl/container/container_stats.go
@@ -0,0 +1,111 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+)
+
+func NewStatsCommand() *cobra.Command {
+ var statsCommand = &cobra.Command{
+ Use: "stats",
+ Short: "Display a live stream of container(s) resource usage statistics.",
+ RunE: statsAction,
+ ValidArgsFunction: statsShellComplete,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+
+ addStatsFlags(statsCommand)
+
+ return statsCommand
+}
+
+func addStatsFlags(cmd *cobra.Command) {
+ cmd.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
+ cmd.Flags().String("format", "", "Pretty-print images using a Go template, e.g, '{{json .}}'")
+ cmd.Flags().Bool("no-stream", false, "Disable streaming stats and only pull the first result")
+ cmd.Flags().Bool("no-trunc", false, "Do not truncate output")
+}
+
+func processStatsCommandFlags(cmd *cobra.Command) (types.ContainerStatsOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.ContainerStatsOptions{}, err
+ }
+
+ all, err := cmd.Flags().GetBool("all")
+ if err != nil {
+ return types.ContainerStatsOptions{}, err
+ }
+
+ noStream, err := cmd.Flags().GetBool("no-stream")
+ if err != nil {
+ return types.ContainerStatsOptions{}, err
+ }
+
+ format, err := cmd.Flags().GetString("format")
+ if err != nil {
+ return types.ContainerStatsOptions{}, err
+ }
+
+ noTrunc, err := cmd.Flags().GetBool("no-trunc")
+ if err != nil {
+ return types.ContainerStatsOptions{}, err
+ }
+
+ return types.ContainerStatsOptions{
+ Stdout: cmd.OutOrStdout(),
+ Stderr: cmd.ErrOrStderr(),
+ GOptions: globalOptions,
+ All: all,
+ Format: format,
+ NoStream: noStream,
+ NoTrunc: noTrunc,
+ }, nil
+}
+
+func statsAction(cmd *cobra.Command, args []string) error {
+ options, err := processStatsCommandFlags(cmd)
+ if err != nil {
+ return err
+ }
+
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
+ if err != nil {
+ return err
+ }
+ defer cancel()
+
+ return container.Stats(ctx, client, args, options)
+}
+
+func statsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ // show running container names
+ statusFilterFn := func(st containerd.ProcessStatus) bool {
+ return st == containerd.Running
+ }
+ return completion.ContainerNames(cmd, statusFilterFn)
+}
diff --git a/cmd/nerdctl/container/container_stats_test.go b/cmd/nerdctl/container/container_stats_test.go
new file mode 100644
index 00000000000..9522e73e009
--- /dev/null
+++ b/cmd/nerdctl/container/container_stats_test.go
@@ -0,0 +1,108 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "runtime"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestStats(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // FIXME: does not seem to work on windows
+ testCase.Require = test.Not(test.Windows)
+
+ if runtime.GOOS == "linux" {
+ // this comment is for `nerdctl ps` but it also valid for `nerdctl stats` :
+ // https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178
+ testCase.Require = test.Require(
+ testCase.Require,
+ nerdtest.CgroupsAccessible,
+ )
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("container"))
+ helpers.Anyhow("rm", "-f", data.Identifier("memlimited"))
+ helpers.Anyhow("rm", "-f", data.Identifier("exited"))
+ }
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-d", "--name", data.Identifier("container"), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("run", "-d", "--name", data.Identifier("memlimited"), "--memory", "1g", testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("run", "--name", data.Identifier("exited"), testutil.CommonImage, "echo", "'exited'")
+ data.Set("id", data.Identifier("container"))
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "stats",
+ Command: test.Command("stats", "--no-stream", "--no-trunc"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("id")),
+ }
+ },
+ },
+ {
+ Description: "container stats",
+ Command: test.Command("container", "stats", "--no-stream", "--no-trunc"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("id")),
+ }
+ },
+ },
+ {
+ Description: "stats ID",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("stats", "--no-stream", data.Get("id"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "container stats ID",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("container", "stats", "--no-stream", data.Get("id"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "no mem limit set",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("stats", "--no-stream")
+ },
+ // https://github.com/containerd/nerdctl/issues/1240
+ // nerdctl used to print UINT64_MAX as the memory limit, so, ensure it does no more
+ Expected: test.Expects(0, nil, test.DoesNotContain("16EiB")),
+ },
+ {
+ Description: "mem limit set",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("stats", "--no-stream")
+ },
+ Expected: test.Expects(0, nil, test.Contains("1GiB")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_stop.go b/cmd/nerdctl/container/container_stop.go
similarity index 82%
rename from cmd/nerdctl/container_stop.go
rename to cmd/nerdctl/container/container_stop.go
index 43a6f6bca44..cbc533b801f 100644
--- a/cmd/nerdctl/container_stop.go
+++ b/cmd/nerdctl/container/container_stop.go
@@ -14,19 +14,23 @@
limitations under the License.
*/
-package main
+package container
import (
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newStopCommand() *cobra.Command {
+func NewStopCommand() *cobra.Command {
var stopCommand = &cobra.Command{
Use: "stop [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -41,7 +45,7 @@ func newStopCommand() *cobra.Command {
}
func processContainerStopOptions(cmd *cobra.Command) (types.ContainerStopOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerStopOptions{}, err
}
@@ -82,5 +86,5 @@ func stopShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st != containerd.Stopped && st != containerd.Created && st != containerd.Unknown
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go
similarity index 50%
rename from cmd/nerdctl/container_stop_linux_test.go
rename to cmd/nerdctl/container/container_stop_linux_test.go
index a3940188ca7..ab03dfd7e45 100644
--- a/cmd/nerdctl/container_stop_linux_test.go
+++ b/cmd/nerdctl/container/container_stop_linux_test.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
@@ -22,10 +22,13 @@ import (
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
-
+ "github.com/coreos/go-iptables/iptables"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
)
func TestStopStart(t *testing.T) {
@@ -70,6 +73,8 @@ func TestStopStart(t *testing.T) {
func TestStopWithStopSignal(t *testing.T) {
t.Parallel()
+ // There may be issues with logs in Docker.
+ // This test is flaky with Docker. Might be related to https://github.com/containerd/nerdctl/pull/3557
base := testutil.NewBase(t)
testContainerName := testutil.Identifier(t)
defer base.Cmd("rm", "-f", testContainerName).Run()
@@ -86,3 +91,71 @@ echo "signal quit"`).AssertOK()
base.Cmd("stop", testContainerName).AssertOK()
base.Cmd("logs", "-f", testContainerName).AssertOutContains("signal quit")
}
+
+func TestStopCleanupForwards(t *testing.T) {
+ const (
+ hostPort = 9999
+ testContainerName = "ngx"
+ )
+ base := testutil.NewBase(t)
+ defer func() {
+ base.Cmd("rm", "-f", testContainerName).Run()
+ }()
+
+ // skip if rootless
+ if rootlessutil.IsRootless() {
+ t.Skip("pkg/testutil/iptables does not support rootless")
+ }
+
+ ipt, err := iptables.New()
+ assert.NilError(t, err)
+
+ containerID := base.Cmd("run", "-d",
+ "--restart=no",
+ "--name", testContainerName,
+ "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort),
+ testutil.NginxAlpineImage).Run().Stdout()
+ containerID = strings.TrimSuffix(containerID, "\n")
+
+ containerIP := base.Cmd("inspect",
+ "-f",
+ "'{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}'",
+ testContainerName).Run().Stdout()
+ containerIP = strings.ReplaceAll(containerIP, "'", "")
+ containerIP = strings.TrimSuffix(containerIP, "\n")
+
+ // define iptables chain name depending on the target (docker/nerdctl)
+ var chain string
+ if testutil.GetTarget() == testutil.Docker {
+ chain = "DOCKER"
+ } else {
+ redirectChain := "CNI-HOSTPORT-DNAT"
+ chain = iptablesutil.GetRedirectedChain(t, ipt, redirectChain, testutil.Namespace, containerID)
+ }
+ assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), true)
+
+ base.Cmd("stop", testContainerName).AssertOK()
+ assert.Equal(t, iptablesutil.ForwardExists(t, ipt, chain, containerIP, hostPort), false)
+}
+
+// Regression test for https://github.com/containerd/nerdctl/issues/3353
+func TestStopCreated(t *testing.T) {
+ t.Parallel()
+
+ base := testutil.NewBase(t)
+ tID := testutil.Identifier(t)
+
+ tearDown := func() {
+ base.Cmd("rm", "-f", tID).Run()
+ }
+
+ setup := func() {
+ base.Cmd("create", "--name", tID, testutil.CommonImage).AssertOK()
+ }
+
+ t.Cleanup(tearDown)
+ tearDown()
+ setup()
+
+ base.Cmd("stop", tID).AssertOK()
+}
diff --git a/pkg/cmd/container/run_cgroup_freebsd.go b/cmd/nerdctl/container/container_test.go
similarity index 74%
rename from pkg/cmd/container/run_cgroup_freebsd.go
rename to cmd/nerdctl/container/container_test.go
index 7ae22379ef9..dcd08829fcd 100644
--- a/pkg/cmd/container/run_cgroup_freebsd.go
+++ b/cmd/nerdctl/container/container_test.go
@@ -17,10 +17,11 @@
package container
import (
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
-func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.SpecOpts, error) {
- return []oci.SpecOpts{}, nil
+func TestMain(m *testing.M) {
+ testutil.M(m)
}
diff --git a/cmd/nerdctl/container_top.go b/cmd/nerdctl/container/container_top.go
similarity index 77%
rename from cmd/nerdctl/container_top.go
rename to cmd/nerdctl/container/container_top.go
index 141a200432e..0f20e45a2c0 100644
--- a/cmd/nerdctl/container_top.go
+++ b/cmd/nerdctl/container/container_top.go
@@ -14,23 +14,26 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
-
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
-func newTopCommand() *cobra.Command {
+func NewTopCommand() *cobra.Command {
var topCommand = &cobra.Command{
Use: "top CONTAINER [ps OPTIONS]",
Args: cobra.MinimumNArgs(1),
@@ -47,7 +50,7 @@ func newTopCommand() *cobra.Command {
func topAction(cmd *cobra.Command, args []string) error {
// NOTE: rootless container does not rely on cgroupv1.
// more details about possible ways to resolve this concern: #223
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -75,5 +78,5 @@ func topShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]s
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st == containerd.Running
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container/container_top_test.go b/cmd/nerdctl/container/container_top_test.go
new file mode 100644
index 00000000000..8042b32a8b4
--- /dev/null
+++ b/cmd/nerdctl/container/container_top_test.go
@@ -0,0 +1,92 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "runtime"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestTop(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ //more details https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178
+ if runtime.GOOS == "linux" {
+ testCase.Require = nerdtest.CgroupsAccessible
+ }
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ // FIXME: busybox 1.36 on windows still appears to not support sleep inf. Unclear why.
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ data.Set("cID", data.Identifier())
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "with o pid,user,cmd",
+ // Docker does not support top -o
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("top", data.Get("cID"), "-o", "pid,user,cmd")
+ },
+
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "simple",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("top", data.Get("cID"))
+ },
+
+ Expected: test.Expects(0, nil, nil),
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestTopHyperVContainer(t *testing.T) {
+
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.HyperV
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ // FIXME: busybox 1.36 on windows still appears to not support sleep inf. Unclear why.
+ helpers.Ensure("run", "--isolation", "hyperv", "-d", "--name", data.Identifier("container"), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("container"))
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("top", data.Identifier("container"))
+ }
+
+ testCase.Expected = test.Expects(0, nil, nil)
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/container_unpause.go b/cmd/nerdctl/container/container_unpause.go
similarity index 79%
rename from cmd/nerdctl/container_unpause.go
rename to cmd/nerdctl/container/container_unpause.go
index 021d3d445b5..8d835c9388e 100644
--- a/cmd/nerdctl/container_unpause.go
+++ b/cmd/nerdctl/container/container_unpause.go
@@ -14,18 +14,21 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
-
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newUnpauseCommand() *cobra.Command {
+func NewUnpauseCommand() *cobra.Command {
var unpauseCommand = &cobra.Command{
Use: "unpause [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -39,7 +42,7 @@ func newUnpauseCommand() *cobra.Command {
}
func processContainerUnpauseOptions(cmd *cobra.Command) (types.ContainerUnpauseOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerUnpauseOptions{}, err
}
@@ -69,5 +72,5 @@ func unpauseShellComplete(cmd *cobra.Command, args []string, toComplete string)
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st == containerd.Paused
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container_update.go b/cmd/nerdctl/container/container_update.go
similarity index 81%
rename from cmd/nerdctl/container_update.go
rename to cmd/nerdctl/container/container_update.go
index 7bebaf2b56c..0ce83e5a6c5 100644
--- a/cmd/nerdctl/container_update.go
+++ b/cmd/nerdctl/container/container_update.go
@@ -14,30 +14,34 @@
limitations under the License.
*/
-package main
+package container
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"runtime"
+ "time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/log"
- "github.com/containerd/containerd/pkg/cri/util"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- nerdctlContainer "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/typeurl/v2"
"github.com/docker/go-units"
runtimespec "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+ "github.com/containerd/typeurl/v2"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ nerdctlContainer "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
)
type updateResourceOptions struct {
@@ -53,7 +57,7 @@ type updateResourceOptions struct {
BlkioWeight uint16
}
-func newUpdateCommand() *cobra.Command {
+func NewUpdateCommand() *cobra.Command {
var updateCommand = &cobra.Command{
Use: "update [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -88,7 +92,7 @@ func setUpdateFlags(cmd *cobra.Command) {
}
func updateAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -196,7 +200,7 @@ func getUpdateOption(cmd *cobra.Command, globalOptions types.GlobalCommandOption
return options, err
}
if kernelMemStr != "" && cmd.Flag("kernel-memory").Changed {
- logrus.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
+ log.L.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
}
cpuset, err := cmd.Flags().GetString("cpuset-cpus")
if err != nil {
@@ -238,7 +242,7 @@ func getUpdateOption(cmd *cobra.Command, globalOptions types.GlobalCommandOption
return options, nil
}
-func updateContainer(ctx context.Context, client *containerd.Client, id string, opts updateResourceOptions, cmd *cobra.Command) error {
+func updateContainer(ctx context.Context, client *containerd.Client, id string, opts updateResourceOptions, cmd *cobra.Command) (retErr error) {
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
@@ -263,16 +267,18 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string,
if spec.Linux.Resources == nil {
spec.Linux.Resources = &runtimespec.LinuxResources{}
}
- if spec.Linux.Resources.BlockIO == nil {
- spec.Linux.Resources.BlockIO = &runtimespec.LinuxBlockIO{}
- }
if cmd.Flags().Changed("blkio-weight") {
+ if spec.Linux.Resources.BlockIO == nil {
+ spec.Linux.Resources.BlockIO = &runtimespec.LinuxBlockIO{}
+ }
if spec.Linux.Resources.BlockIO.Weight != &opts.BlkioWeight {
spec.Linux.Resources.BlockIO.Weight = &opts.BlkioWeight
}
}
- if spec.Linux.Resources.CPU == nil {
- spec.Linux.Resources.CPU = &runtimespec.LinuxCPU{}
+ if cmd.Flags().Changed("cpu-shares") || cmd.Flags().Changed("cpu-quota") || cmd.Flags().Changed("cpu-period") || cmd.Flags().Changed("cpus") || cmd.Flags().Changed("cpuset-mems") || cmd.Flags().Changed("cpuset-cpus") {
+ if spec.Linux.Resources.CPU == nil {
+ spec.Linux.Resources.CPU = &runtimespec.LinuxCPU{}
+ }
}
if cmd.Flags().Changed("cpu-shares") {
if spec.Linux.Resources.CPU.Shares != &opts.CPUShares {
@@ -305,8 +311,10 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string,
spec.Linux.Resources.CPU.Cpus = opts.CpusetCpus
}
}
- if spec.Linux.Resources.Memory == nil {
- spec.Linux.Resources.Memory = &runtimespec.LinuxMemory{}
+ if cmd.Flags().Changed("memory") || cmd.Flags().Changed("memory-reservation") {
+ if spec.Linux.Resources.Memory == nil {
+ spec.Linux.Resources.Memory = &runtimespec.LinuxMemory{}
+ }
}
if cmd.Flags().Changed("memory") {
if spec.Linux.Resources.Memory.Limit != &opts.MemoryLimitInBytes {
@@ -321,10 +329,10 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string,
spec.Linux.Resources.Memory.Reservation = &opts.MemoryReservation
}
}
- if spec.Linux.Resources.Pids == nil {
- spec.Linux.Resources.Pids = &runtimespec.LinuxPids{}
- }
if cmd.Flags().Changed("pids-limit") {
+ if spec.Linux.Resources.Pids == nil {
+ spec.Linux.Resources.Pids = &runtimespec.LinuxPids{}
+ }
if spec.Linux.Resources.Pids.Limit != opts.PidsLimit {
spec.Linux.Resources.Pids.Limit = opts.PidsLimit
}
@@ -332,12 +340,18 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string,
}
if err := updateContainerSpec(ctx, container, spec); err != nil {
- log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", spec, id)
- // reset spec on error.
- if err := updateContainerSpec(ctx, container, oldSpec); err != nil {
- log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", oldSpec, id)
+ return fmt.Errorf("failed to update spec %+v for container %q", spec, id)
+ }
+ defer func() {
+ if retErr != nil {
+ deferCtx, deferCancel := context.WithTimeout(ctx, 1*time.Minute)
+ defer deferCancel()
+ // Reset spec on error.
+ if err := updateContainerSpec(deferCtx, container, oldSpec); err != nil {
+ log.G(ctx).WithError(err).Errorf("Failed to update spec %+v for container %q", oldSpec, id)
+ }
}
- }
+ }()
restart, err := cmd.Flags().GetString("restart")
if err != nil {
@@ -362,19 +376,16 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string,
}
return fmt.Errorf("failed to get task:%w", err)
}
- if err := task.Update(ctx, containerd.WithResources(spec.Linux.Resources)); err != nil {
- return err
- }
- return nil
+ return task.Update(ctx, containerd.WithResources(spec.Linux.Resources))
}
func updateContainerSpec(ctx context.Context, container containerd.Container, spec *runtimespec.Spec) error {
if err := container.Update(ctx, func(ctx context.Context, client *containerd.Client, c *containers.Container) error {
- any, err := typeurl.MarshalAny(spec)
+ a, err := typeurl.MarshalAny(spec)
if err != nil {
return fmt.Errorf("failed to marshal spec %+v:%w", spec, err)
}
- c.Spec = any
+ c.Spec = a
return nil
}); err != nil {
return fmt.Errorf("failed to update container spec:%w", err)
@@ -384,12 +395,20 @@ func updateContainerSpec(ctx context.Context, container containerd.Container, sp
func copySpec(spec *runtimespec.Spec) (*runtimespec.Spec, error) {
var copySpec runtimespec.Spec
- if err := util.DeepCopy(©Spec, spec); err != nil {
- return nil, fmt.Errorf("failed to deep copy:%w", err)
+ if spec == nil {
+ return nil, errors.New("spec cannot be nil")
+ }
+ bytes, err := json.Marshal(spec)
+ if err != nil {
+ return nil, fmt.Errorf("unable to marshal spec: %w", err)
+ }
+ err = json.Unmarshal(bytes, ©Spec)
+ if err != nil {
+ return nil, fmt.Errorf("unable to unmarshal into spec copy: %w", err)
}
return ©Spec, nil
}
func updateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return shellCompleteContainerNames(cmd, nil)
+ return completion.ContainerNames(cmd, nil)
}
diff --git a/cmd/nerdctl/builder_linux_test.go b/cmd/nerdctl/container/container_update_linux_test.go
similarity index 57%
rename from cmd/nerdctl/builder_linux_test.go
rename to cmd/nerdctl/container/container_update_linux_test.go
index 5376698242d..a4091f4156a 100644
--- a/cmd/nerdctl/builder_linux_test.go
+++ b/cmd/nerdctl/container/container_update_linux_test.go
@@ -14,29 +14,20 @@
limitations under the License.
*/
-package main
+package container
import (
- "bytes"
- "fmt"
- "os"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
-func TestBuilderDebug(t *testing.T) {
+func TestUpdateContainer(t *testing.T) {
testutil.DockerIncompatible(t)
+ testContainerName := testutil.Identifier(t)
base := testutil.NewBase(t)
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-builder-debug-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("builder", "debug", buildCtx).CmdOption(testutil.WithStdin(bytes.NewReader([]byte("c\n")))).AssertOK()
+ base.Cmd("run", "-d", "--name", testContainerName, testutil.CommonImage, "sleep", "infinity").AssertOK()
+ defer base.Cmd("rm", "-f", testContainerName).Run()
+ base.Cmd("update", "--memory", "999999999", "--restart", "123", testContainerName).AssertFail()
+ base.Cmd("inspect", "--mode=native", testContainerName).AssertOutNotContains(`"limit": 999999999,`)
}
diff --git a/cmd/nerdctl/container_wait.go b/cmd/nerdctl/container/container_wait.go
similarity index 79%
rename from cmd/nerdctl/container_wait.go
rename to cmd/nerdctl/container/container_wait.go
index ae4ce9ffc3d..c28168b090d 100644
--- a/cmd/nerdctl/container_wait.go
+++ b/cmd/nerdctl/container/container_wait.go
@@ -14,17 +14,21 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
)
-func newWaitCommand() *cobra.Command {
+func NewWaitCommand() *cobra.Command {
var waitCommand = &cobra.Command{
Use: "wait [flags] CONTAINER [CONTAINER, ...]",
Args: cobra.MinimumNArgs(1),
@@ -38,7 +42,7 @@ func newWaitCommand() *cobra.Command {
}
func processContainerWaitOptions(cmd *cobra.Command) (types.ContainerWaitOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ContainerWaitOptions{}, err
}
@@ -68,5 +72,5 @@ func waitShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]
statusFilterFn := func(st containerd.ProcessStatus) bool {
return st == containerd.Running
}
- return shellCompleteContainerNames(cmd, statusFilterFn)
+ return completion.ContainerNames(cmd, statusFilterFn)
}
diff --git a/cmd/nerdctl/container/container_wait_test.go b/cmd/nerdctl/container/container_wait_test.go
new file mode 100644
index 00000000000..9c0c93da3ff
--- /dev/null
+++ b/cmd/nerdctl/container/container_wait_test.go
@@ -0,0 +1,50 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestWait(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("1"), data.Identifier("2"), data.Identifier("3"))
+ }
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-d", "--name", data.Identifier("1"), testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", data.Identifier("2"), testutil.CommonImage, "sleep", "1")
+ helpers.Ensure("run", "-d", "--name", data.Identifier("3"), testutil.CommonImage, "sh", "-euxc", "sleep 5; exit 123")
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("wait", data.Identifier("1"), data.Identifier("2"), data.Identifier("3"))
+ }
+
+ testCase.Expected = test.Expects(0, nil, test.Equals(`0
+0
+123
+`))
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/multi_platform_linux_test.go b/cmd/nerdctl/container/multi_platform_linux_test.go
similarity index 83%
rename from cmd/nerdctl/multi_platform_linux_test.go
rename to cmd/nerdctl/container/multi_platform_linux_test.go
index 017d2827c8b..3acb0e3ef87 100644
--- a/cmd/nerdctl/multi_platform_linux_test.go
+++ b/cmd/nerdctl/container/multi_platform_linux_test.go
@@ -14,19 +14,20 @@
limitations under the License.
*/
-package main
+package container
import (
"fmt"
"io"
- "os"
"strings"
"testing"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
)
func testMultiPlatformRun(base *testutil.Base, alpineImage string) {
@@ -54,23 +55,21 @@ func TestMultiPlatformRun(t *testing.T) {
func TestMultiPlatformBuildPush(t *testing.T) {
testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform.
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7")
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
+ reg := testregistry.NewWithNoAuth(base, 0, false)
+ defer reg.Cleanup(nil)
- imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.ListenPort, tID)
+ imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID)
defer base.Cmd("rmi", imageName).Run()
dockerfile := fmt.Sprintf(`FROM %s
RUN echo dummy
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK()
testMultiPlatformRun(base, imageName)
@@ -83,23 +82,21 @@ RUN echo dummy
func TestMultiPlatformBuildPushNoRun(t *testing.T) {
testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform.
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7")
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
+ reg := testregistry.NewWithNoAuth(base, 0, false)
+ defer reg.Cleanup(nil)
- imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.ListenPort, tID)
+ imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID)
defer base.Cmd("rmi", imageName).Run()
dockerfile := fmt.Sprintf(`FROM %s
CMD echo dummy
`, testutil.AlpineImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
+ buildCtx := helpers.CreateBuildContext(t, dockerfile)
base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK()
testMultiPlatformRun(base, imageName)
@@ -110,10 +107,10 @@ func TestMultiPlatformPullPushAllPlatforms(t *testing.T) {
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
+ reg := testregistry.NewWithNoAuth(base, 0, false)
+ defer reg.Cleanup(nil)
- pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.ListenPort, tID)
+ pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID)
defer base.Cmd("rmi", pushImageName).Run()
base.Cmd("pull", "--all-platforms", testutil.AlpineImage).AssertOK()
@@ -125,9 +122,9 @@ func TestMultiPlatformPullPushAllPlatforms(t *testing.T) {
func TestMultiPlatformComposeUpBuild(t *testing.T) {
testutil.DockerIncompatible(t)
testutil.RequiresBuild(t)
+ testutil.RegisterBuildCacheCleanup(t)
testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7")
base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
const dockerComposeYAML = `
services:
diff --git a/cmd/nerdctl/container_commit_test.go b/cmd/nerdctl/container_commit_test.go
deleted file mode 100644
index c48209f1d24..00000000000
--- a/cmd/nerdctl/container_commit_test.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestCommit(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- switch base.Info().CgroupDriver {
- case "none", "":
- t.Skip("requires cgroup (for pausing)")
- }
- testContainer := testutil.Identifier(t)
- testImage := testutil.Identifier(t) + "-img"
- defer base.Cmd("rm", "-f", testContainer).Run()
- defer base.Cmd("rmi", testImage).Run()
-
- for _, pause := range []string{
- "true",
- "false",
- } {
- base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "infinity").AssertOK()
- base.EnsureContainerStarted(testContainer)
- base.Cmd("exec", testContainer, "sh", "-euxc", `echo hello-test-commit > /foo`).AssertOK()
- base.Cmd(
- "commit",
- "-c", `CMD ["/foo"]`,
- "-c", `ENTRYPOINT ["cat"]`,
- fmt.Sprintf("--pause=%s", pause),
- testContainer, testImage).AssertOK()
- base.Cmd("run", "--rm", testImage).AssertOutExactly("hello-test-commit\n")
- base.Cmd("rm", "-f", testContainer).Run()
- base.Cmd("rmi", testImage).Run()
- }
-}
diff --git a/cmd/nerdctl/container_cp.go b/cmd/nerdctl/container_cp.go
deleted file mode 100644
index a80135b16bd..00000000000
--- a/cmd/nerdctl/container_cp.go
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "errors"
- "path/filepath"
- "strings"
-
- "github.com/spf13/cobra"
-)
-
-var errFileSpecDoesntMatchFormat = errors.New("filespec must match the canonical format: [container:]file/path")
-
-func parseCpFileSpec(arg string) (*cpFileSpec, error) {
- i := strings.Index(arg, ":")
-
- // filespec starting with a semicolon is invalid
- if i == 0 {
- return nil, errFileSpecDoesntMatchFormat
- }
-
- if filepath.IsAbs(arg) {
- // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
- return &cpFileSpec{
- Container: nil,
- Path: arg,
- }, nil
- }
-
- parts := strings.SplitN(arg, ":", 2)
-
- if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
- // Either there's no `:` in the arg
- // OR it's an explicit local relative path like `./file:name.txt`.
- return &cpFileSpec{
- Path: arg,
- }, nil
- }
-
- return &cpFileSpec{
- Container: &parts[0],
- Path: parts[1],
- }, nil
-}
-
-type cpFileSpec struct {
- Container *string
- Path string
-}
-
-func cpShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveFilterFileExt
-}
diff --git a/cmd/nerdctl/container_cp_linux_test.go b/cmd/nerdctl/container_cp_linux_test.go
deleted file mode 100644
index 5442ed53b30..00000000000
--- a/cmd/nerdctl/container_cp_linux_test.go
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "syscall"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestCopyToContainer(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- testContainer := testutil.Identifier(t)
-
- base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK()
- defer base.Cmd("rm", "-f", testContainer).Run()
-
- srcUID := os.Geteuid()
- srcDir := t.TempDir()
- srcFile := filepath.Join(srcDir, "test-file")
- srcFileContent := []byte("test-file-content")
- err := os.WriteFile(srcFile, srcFileContent, 0644)
- assert.NilError(t, err)
-
- assertCat := func(catPath string) {
- t.Logf("catPath=%q", catPath)
- base.Cmd("exec", testContainer, "cat", catPath).AssertOutExactly(string(srcFileContent))
- base.Cmd("exec", testContainer, "stat", "-c", "%u", catPath).AssertOutExactly(fmt.Sprintf("%d\n", srcUID))
- }
-
- // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/
- t.Run("SRC_PATH specifies a file", func(t *testing.T) {
- srcPath := srcFile
- t.Run("DEST_PATH does not exist", func(t *testing.T) {
- destPath := "/dest-no-exist-no-slash"
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := destPath
- assertCat(catPath)
- })
- t.Run("DEST_PATH does not exist and ends with /", func(t *testing.T) {
- destPath := "/dest-no-exist-with-slash/"
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail()
- })
- t.Run("DEST_PATH exists and is a file", func(t *testing.T) {
- destPath := "/dest-file-exists"
- base.Cmd("exec", testContainer, "touch", destPath).AssertOK()
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := destPath
- assertCat(catPath)
- })
- t.Run("DEST_PATH exists and is a directory", func(t *testing.T) {
- destPath := "/dest-dir-exists"
- base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK()
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- assertCat(catPath)
- })
- })
- t.Run("SRC_PATH specifies a directory", func(t *testing.T) {
- srcPath := srcDir
- t.Run("DEST_PATH does not exist", func(t *testing.T) {
- destPath := "/dest2-no-exist"
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- assertCat(catPath)
- })
- t.Run("DEST_PATH exists and is a file", func(t *testing.T) {
- destPath := "/dest2-file-exists"
- base.Cmd("exec", testContainer, "touch", destPath).AssertOK()
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail()
- })
- t.Run("DEST_PATH exists and is a directory", func(t *testing.T) {
- t.Run("SRC_PATH does not end with `/.`", func(t *testing.T) {
- destPath := "/dest2-dir-exists"
- base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK()
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := filepath.Join(destPath, strings.TrimPrefix(srcFile, filepath.Dir(srcDir)+"/"))
- assertCat(catPath)
- })
- t.Run("SRC_PATH does end with `/.`", func(t *testing.T) {
- srcPath += "/."
- destPath := "/dest2-dir2-exists"
- base.Cmd("exec", testContainer, "mkdir", "-p", destPath).AssertOK()
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- t.Logf("catPath=%q", catPath)
- assertCat(catPath)
- })
- })
- })
-}
-
-func TestCopyFromContainer(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- testContainer := testutil.Identifier(t)
-
- base.Cmd("run", "-d", "--name", testContainer, testutil.CommonImage, "sleep", "1h").AssertOK()
- defer base.Cmd("rm", "-f", testContainer).Run()
-
- euid := os.Geteuid()
- srcUID := 42
- srcDir := "/test-dir"
- srcFile := filepath.Join(srcDir, "test-file")
- srcFileContent := []byte("test-file-content")
- mkSrcScript := fmt.Sprintf("mkdir -p %q && echo -n %q >%q && chown %d %q", srcDir, srcFileContent, srcFile, srcUID, srcFile)
- base.Cmd("exec", testContainer, "sh", "-euc", mkSrcScript).AssertOK()
-
- assertCat := func(catPath string) {
- t.Logf("catPath=%q", catPath)
- got, err := os.ReadFile(catPath)
- assert.NilError(t, err)
- assert.DeepEqual(t, srcFileContent, got)
- st, err := os.Stat(catPath)
- assert.NilError(t, err)
- stSys := st.Sys().(*syscall.Stat_t)
- // stSys.Uid matches euid, not srcUID
- assert.DeepEqual(t, uint32(euid), stSys.Uid)
- }
-
- td := t.TempDir()
- // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/
- t.Run("SRC_PATH specifies a file", func(t *testing.T) {
- srcPath := srcFile
- t.Run("DEST_PATH does not exist", func(t *testing.T) {
- destPath := filepath.Join(td, "dest-no-exist-no-slash")
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := destPath
- assertCat(catPath)
- })
- t.Run("DEST_PATH does not exist and ends with /", func(t *testing.T) {
- destPath := td + "/dest-no-exist-with-slash/" // Avoid filepath.Join, to forcibly append "/"
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertFail()
- })
- t.Run("DEST_PATH exists and is a file", func(t *testing.T) {
- destPath := filepath.Join(td, "dest-file-exists")
- err := os.WriteFile(destPath, []byte(""), 0644)
- assert.NilError(t, err)
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := destPath
- assertCat(catPath)
- })
- t.Run("DEST_PATH exists and is a directory", func(t *testing.T) {
- destPath := filepath.Join(td, "dest-dir-exists")
- err := os.Mkdir(destPath, 0755)
- assert.NilError(t, err)
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- assertCat(catPath)
- })
- })
- t.Run("SRC_PATH specifies a directory", func(t *testing.T) {
- srcPath := srcDir
- t.Run("DEST_PATH does not exist", func(t *testing.T) {
- destPath := filepath.Join(td, "dest2-no-exist")
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- assertCat(catPath)
- })
- t.Run("DEST_PATH exists and is a file", func(t *testing.T) {
- destPath := filepath.Join(td, "dest2-file-exists")
- err := os.WriteFile(destPath, []byte(""), 0644)
- assert.NilError(t, err)
- base.Cmd("cp", srcPath, testContainer+":"+destPath).AssertFail()
- })
- t.Run("DEST_PATH exists and is a directory", func(t *testing.T) {
- t.Run("SRC_PATH does not end with `/.`", func(t *testing.T) {
- destPath := filepath.Join(td, "dest2-dir-exists")
- err := os.Mkdir(destPath, 0755)
- assert.NilError(t, err)
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := filepath.Join(destPath, strings.TrimPrefix(srcFile, filepath.Dir(srcDir)+"/"))
- assertCat(catPath)
- })
- t.Run("SRC_PATH does end with `/.`", func(t *testing.T) {
- srcPath += "/."
- destPath := filepath.Join(td, "dest2-dir2-exists")
- err := os.Mkdir(destPath, 0755)
- assert.NilError(t, err)
- base.Cmd("cp", testContainer+":"+srcPath, destPath).AssertOK()
- catPath := filepath.Join(destPath, filepath.Base(srcFile))
- assertCat(catPath)
- })
- })
- })
-}
diff --git a/cmd/nerdctl/container_create_linux_test.go b/cmd/nerdctl/container_create_linux_test.go
deleted file mode 100644
index 403d463de8d..00000000000
--- a/cmd/nerdctl/container_create_linux_test.go
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "runtime"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
- "gotest.tools/v3/assert"
-)
-
-func TestCreate(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- base.Cmd("create", "--name", tID, testutil.CommonImage, "echo", "foo").AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
- base.Cmd("ps", "-a").AssertOutContains("Created")
- base.Cmd("start", tID).AssertOK()
- base.Cmd("logs", tID).AssertOutContains("foo")
-}
-
-func TestCreateWithMACAddress(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- networkBridge := "testNetworkBridge" + tID
- networkMACvlan := "testNetworkMACvlan" + tID
- networkIPvlan := "testNetworkIPvlan" + tID
- base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK()
- base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK()
- base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK()
- t.Cleanup(func() {
- base.Cmd("network", "rm", networkBridge).Run()
- base.Cmd("network", "rm", networkMACvlan).Run()
- base.Cmd("network", "rm", networkIPvlan).Run()
- })
- tests := []struct {
- Network string
- WantErr bool
- Expect string
- }{
- {"host", true, "conflicting options"},
- {"none", true, "can't open '/sys/class/net/eth0/address'"},
- {"container:whatever" + tID, true, "conflicting options"},
- {"bridge", false, ""},
- {networkBridge, false, ""},
- {networkMACvlan, false, ""},
- {networkIPvlan, true, "not support"},
- }
- for i, test := range tests {
- containerName := fmt.Sprintf("%s_%d", tID, i)
- testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect)
- t.Run(testName, func(tt *testing.T) {
- macAddress, err := nettestutil.GenerateMACAddress()
- if err != nil {
- tt.Errorf("failed to generate MAC address: %s", err)
- }
- if test.Expect == "" && !test.WantErr {
- test.Expect = macAddress
- }
- tt.Cleanup(func() {
- base.Cmd("rm", "-f", containerName).Run()
- })
- cmd := base.Cmd("create", "--network", test.Network, "--mac-address", macAddress, "--name", containerName, testutil.CommonImage, "cat", "/sys/class/net/eth0/address")
- if !test.WantErr {
- cmd.AssertOK()
- base.Cmd("start", containerName).AssertOK()
- cmd = base.Cmd("logs", containerName)
- cmd.AssertOK()
- cmd.AssertOutContains(test.Expect)
- } else {
- if (testutil.GetTarget() == testutil.Docker && test.Network == networkIPvlan) || test.Network == "none" {
- // 1. unlike nerdctl
- // when using network ipvlan in Docker
- // it delays fail on executing start command
- // 2. start on network none will success in both
- // nerdctl and Docker
- cmd.AssertOK()
- cmd = base.Cmd("start", containerName)
- if test.Network == "none" {
- // we check the result on logs command
- cmd.AssertOK()
- cmd = base.Cmd("logs", containerName)
- }
- }
- cmd.AssertCombinedOutContains(test.Expect)
- if test.Network == "none" {
- cmd.AssertOK()
- } else {
- cmd.AssertFail()
- }
- }
- })
- }
-}
-
-func TestCreateWithTty(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("json-file log driver is not yet implemented on Windows")
- }
- base := testutil.NewBase(t)
- imageName := testutil.CommonImage
- withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t)
- withTtyContainerName := "with-terminal-" + testutil.Identifier(t)
-
- // without -t, fail
- base.Cmd("create", "--name", withoutTtyContainerName, imageName, "stty").AssertOK()
- base.Cmd("start", withoutTtyContainerName).AssertOK()
- defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK()
- base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty")
- withoutTtyContainer := base.InspectContainer(withoutTtyContainerName)
- assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode)
-
- // with -t, success
- base.Cmd("create", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK()
- base.Cmd("start", withTtyContainerName).AssertOK()
- defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK()
- base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;")
- withTtyContainer := base.InspectContainer(withTtyContainerName)
- assert.Equal(base.T, 0, withTtyContainer.State.ExitCode)
-}
diff --git a/cmd/nerdctl/container_create_windows_test.go b/cmd/nerdctl/container_create_windows_test.go
deleted file mode 100644
index e8f662895ac..00000000000
--- a/cmd/nerdctl/container_create_windows_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
- "time"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestCreateProcessContainer(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- base.Cmd("create", "--name", tID, testutil.CommonImage, "echo", "foo").AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
- base.Cmd("ps", "-a").AssertOutContains("Created")
- base.Cmd("start", tID).AssertOK()
- base.Cmd("logs", tID).AssertOutContains("foo")
-}
-
-func TestCreateHyperVContainer(t *testing.T) {
- //t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- if !testutil.HyperVSupported() {
- t.Skip("HyperV is not enabled, skipping test")
- }
-
- base.Cmd("create", "--isolation", "hyperv", "--name", tID, testutil.CommonImage, "echo", "foo").AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
- base.Cmd("ps", "-a").AssertOutContains("Created")
-
- base.Cmd("start", tID).AssertOK()
- // hyperv containers take a few seconds to fire up, the test would fail without the sleep
- // EnsureContainerStarted does not work
- time.Sleep(10 * time.Second)
-
- base.Cmd("logs", tID).AssertOutContains("foo")
-}
diff --git a/cmd/nerdctl/container_list.go b/cmd/nerdctl/container_list.go
deleted file mode 100644
index 8c8ad9011dc..00000000000
--- a/cmd/nerdctl/container_list.go
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
-
- "github.com/spf13/cobra"
-)
-
-func newPsCommand() *cobra.Command {
- var psCommand = &cobra.Command{
- Use: "ps",
- Args: cobra.NoArgs,
- Short: "List containers",
- RunE: psAction,
- SilenceUsage: true,
- SilenceErrors: true,
- }
- psCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
- psCommand.Flags().IntP("last", "n", -1, "Show n last created containers (includes all states)")
- psCommand.Flags().BoolP("latest", "l", false, "Show the latest created container (includes all states)")
- psCommand.Flags().Bool("no-trunc", false, "Don't truncate output")
- psCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs")
- psCommand.Flags().BoolP("size", "s", false, "Display total file sizes")
-
- // Alias "-f" is reserved for "--filter"
- psCommand.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}', 'wide'")
- psCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp
- })
- psCommand.Flags().StringSliceP("filter", "f", nil, "Filter matches containers based on given conditions")
- return psCommand
-}
-
-func processContainerListOptions(cmd *cobra.Command) (types.ContainerListOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- all, err := cmd.Flags().GetBool("all")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- latest, err := cmd.Flags().GetBool("latest")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
-
- lastN, err := cmd.Flags().GetInt("last")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- if lastN == -1 && latest {
- lastN = 1
- }
-
- filters, err := cmd.Flags().GetStringSlice("filter")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
-
- noTrunc, err := cmd.Flags().GetBool("no-trunc")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- trunc := !noTrunc
-
- quiet, err := cmd.Flags().GetBool("quiet")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- format, err := cmd.Flags().GetString("format")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
-
- size := false
- if !quiet {
- size, err = cmd.Flags().GetBool("size")
- if err != nil {
- return types.ContainerListOptions{}, err
- }
- }
-
- return types.ContainerListOptions{
- Stdout: cmd.OutOrStdout(),
- GOptions: globalOptions,
- All: all,
- LastN: lastN,
- Truncate: trunc,
- Quiet: quiet,
- Size: size,
- Format: format,
- Filters: filters,
- }, nil
-}
-
-func psAction(cmd *cobra.Command, args []string) error {
- options, err := processContainerListOptions(cmd)
- if err != nil {
- return err
- }
-
- client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
- if err != nil {
- return err
- }
- defer cancel()
-
- return container.List(ctx, client, options)
-}
diff --git a/cmd/nerdctl/container_prune_linux_test.go b/cmd/nerdctl/container_prune_linux_test.go
deleted file mode 100644
index bfcda034719..00000000000
--- a/cmd/nerdctl/container_prune_linux_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestPruneContainer(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- base.Cmd("run", "-d", "--name", tID+"-1", testutil.CommonImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID+"-1").Run()
- base.Cmd("create", "--name", tID+"-2", testutil.CommonImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID+"-2").Run()
-
- base.Cmd("container", "prune", "-f").AssertOK()
- // tID-1 is still running, tID-2 is not
- base.Cmd("inspect", tID+"-1").AssertOK()
- base.Cmd("inspect", tID+"-2").AssertFail()
-
- base.Cmd("kill", tID+"-1").AssertOK()
- base.Cmd("container", "prune", "-f").AssertOK()
- base.Cmd("inspect", tID+"-1").AssertFail()
-}
diff --git a/cmd/nerdctl/container_stats_linux_test.go b/cmd/nerdctl/container_stats_linux_test.go
deleted file mode 100644
index 738bb855551..00000000000
--- a/cmd/nerdctl/container_stats_linux_test.go
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestStats(t *testing.T) {
- // this comment is for `nerdctl ps` but it also valid for `nerdctl stats` :
- // https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178
- if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
- t.Skip("test skipped for rootless containers on cgroup v1")
- }
- testContainerName := testutil.Identifier(t)[:12]
- exitedTestContainerName := fmt.Sprintf("%s-exited", testContainerName)
-
- base := testutil.NewBase(t)
- defer base.Cmd("rm", "-f", testContainerName).Run()
- defer base.Cmd("rm", "-f", exitedTestContainerName).Run()
- base.Cmd("run", "--name", exitedTestContainerName, testutil.AlpineImage, "echo", "'exited'").AssertOK()
-
- base.Cmd("run", "-d", "--name", testContainerName, testutil.AlpineImage, "sleep", "5").AssertOK()
- base.Cmd("stats", "--no-stream").AssertOutContains(testContainerName)
- base.Cmd("stats", "--no-stream", testContainerName).AssertOK()
-}
diff --git a/cmd/nerdctl/container_top_unix_test.go b/cmd/nerdctl/container_top_unix_test.go
deleted file mode 100644
index acf3676362b..00000000000
--- a/cmd/nerdctl/container_top_unix_test.go
+++ /dev/null
@@ -1,43 +0,0 @@
-//go:build linux || darwin || freebsd || netbsd || openbsd
-
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestTop(t *testing.T) {
- t.Parallel()
- //more details https://github.com/containerd/nerdctl/pull/223#issuecomment-851395178
- if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
- t.Skip("test skipped for rootless containers on cgroup v1")
- }
- testContainerName := testutil.Identifier(t)
-
- base := testutil.NewBase(t)
- defer base.Cmd("rm", "-f", testContainerName).Run()
-
- base.Cmd("run", "-d", "--name", testContainerName, testutil.AlpineImage, "sleep", "5").AssertOK()
- base.Cmd("top", testContainerName, "-o", "pid,user,cmd").AssertOK()
-
-}
diff --git a/cmd/nerdctl/container_top_windows_test.go b/cmd/nerdctl/container_top_windows_test.go
deleted file mode 100644
index f862cf328ae..00000000000
--- a/cmd/nerdctl/container_top_windows_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestTopProcessContainer(t *testing.T) {
- testContainerName := testutil.Identifier(t)
-
- base := testutil.NewBase(t)
- defer base.Cmd("rm", "-f", testContainerName).Run()
-
- base.Cmd("run", "-d", "--name", testContainerName, testutil.WindowsNano, "sleep", "5").AssertOK()
- base.Cmd("top", testContainerName).AssertOK()
-}
-
-func TestTopHyperVContainer(t *testing.T) {
- if !testutil.HyperVSupported() {
- t.Skip("HyperV is not enabled, skipping test")
- }
-
- testContainerName := testutil.Identifier(t)
-
- base := testutil.NewBase(t)
- defer base.Cmd("rm", "-f", testContainerName).Run()
-
- base.Cmd("run", "--isolation", "hyperv", "-d", "--name", testContainerName, testutil.WindowsNano, "sleep", "5").AssertOK()
- base.Cmd("top", testContainerName).AssertOK()
-}
diff --git a/cmd/nerdctl/container_wait_test.go b/cmd/nerdctl/container_wait_test.go
deleted file mode 100644
index 6a580cf143b..00000000000
--- a/cmd/nerdctl/container_wait_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestWait(t *testing.T) {
- t.Parallel()
- tID := testutil.Identifier(t)
- testContainerName1 := tID + "-1"
- testContainerName2 := tID + "-2"
- testContainerName3 := tID + "-3"
-
- const expected = `0
-0
-123
-`
- base := testutil.NewBase(t)
- defer base.Cmd("rm", "-f", testContainerName1, testContainerName2, testContainerName3).Run()
-
- base.Cmd("run", "-d", "--name", testContainerName1, testutil.CommonImage, "sleep", "1").AssertOK()
-
- base.Cmd("run", "-d", "--name", testContainerName2, testutil.CommonImage, "sleep", "1").AssertOK()
-
- base.Cmd("run", "--name", testContainerName3, testutil.CommonImage, "sh", "-euxc", "sleep 5; exit 123").AssertExitCode(123)
-
- base.Cmd("wait", testContainerName1, testContainerName2, testContainerName3).AssertOutExactly(expected)
-
-}
diff --git a/cmd/nerdctl/helpers/cobra.go b/cmd/nerdctl/helpers/cobra.go
new file mode 100644
index 00000000000..d35030ea8cf
--- /dev/null
+++ b/cmd/nerdctl/helpers/cobra.go
@@ -0,0 +1,285 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package helpers
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strconv"
+ "time"
+
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "github.com/containerd/log"
+)
+
+// UnknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail
+// https://github.com/containerd/nerdctl/issues/487
+//
+// Ideally this should be implemented in Cobra itself.
+func UnknownSubcommandAction(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return cmd.Help()
+ }
+ // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662
+ msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name())
+ if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
+ msg += "\n\nDid you mean this?\n"
+ for _, s := range suggestions {
+ msg += fmt.Sprintf("\t%v\n", s)
+ }
+ }
+ return errors.New(msg)
+}
+
+// IsExactArgs returns an error if there is not the exact number of args
+func IsExactArgs(number int) cobra.PositionalArgs {
+ return func(cmd *cobra.Command, args []string) error {
+ if len(args) == number {
+ return nil
+ }
+ return fmt.Errorf(
+ "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
+ cmd.CommandPath(),
+ number,
+ "argument(s)",
+ cmd.CommandPath(),
+ cmd.UseLine(),
+ cmd.Short,
+ )
+ }
+}
+
+// AddStringFlag is similar to cmd.Flags().String but supports aliases and env var
+func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ value = envV
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new(string)
+ flags := cmd.Flags()
+ flags.StringVar(p, name, value, usage)
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.StringVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.StringVar(p, a, value, aliasesUsage)
+ }
+ }
+}
+
+// AddIntFlag is similar to cmd.Flags().Int but supports aliases and env var
+func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, env, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ v, err := strconv.ParseInt(envV, 10, 64)
+ if err != nil {
+ log.L.WithError(err).Warnf("Invalid int value for `%s`", env)
+ }
+ value = int(v)
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new(int)
+ flags := cmd.Flags()
+ flags.IntVar(p, name, value, usage)
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.IntVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.IntVar(p, a, value, aliasesUsage)
+ }
+ }
+}
+
+// AddDurationFlag is similar to cmd.Flags().Duration but supports aliases and env var
+func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value time.Duration, env, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ var err error
+ value, err = time.ParseDuration(envV)
+ if err != nil {
+ log.L.WithError(err).Warnf("Invalid duration value for `%s`", env)
+ }
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new(time.Duration)
+ flags := cmd.Flags()
+ flags.DurationVar(p, name, value, usage)
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.DurationVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.DurationVar(p, a, value, aliasesUsage)
+ }
+ }
+}
+
+func GlobalFlags(cmd *cobra.Command) (string, []string) {
+ args0, err := os.Executable()
+ if err != nil {
+ log.L.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0])
+ args0 = os.Args[0]
+ }
+ if len(os.Args) < 2 {
+ return args0, nil
+ }
+
+ rootCmd := cmd.Root()
+ flagSet := rootCmd.Flags()
+ args := []string{}
+ flagSet.VisitAll(func(f *pflag.Flag) {
+ key := f.Name
+ val := f.Value.String()
+ if f.Changed {
+ args = append(args, "--"+key+"="+val)
+ }
+ })
+ return args0, args
+}
+
+// AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent.
+// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
+func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ value = []string{envV}
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new([]string)
+ flags := cmd.Flags()
+ for _, a := range nonPersistentAliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.StringArrayVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.StringArrayVar(p, a, value, aliasesUsage)
+ }
+ }
+
+ persistentFlags := cmd.PersistentFlags()
+ persistentFlags.StringArrayVar(p, name, value, usage)
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage)
+ } else {
+ persistentFlags.StringArrayVar(p, a, value, aliasesUsage)
+ }
+ }
+}
+
+// AddPersistentStringFlag is similar to AddStringFlag but persistent.
+// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
+func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ value = envV
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new(string)
+
+ // flags is full set of flag(s)
+ // flags can redefine alias already used in subcommands
+ flags := cmd.Flags()
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.StringVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.StringVar(p, a, value, aliasesUsage)
+ }
+ // non-persistent flags are not added to the InheritedFlags, so we should add them manually
+ f := flags.Lookup(a)
+ aliasToBeInherited.AddFlag(f)
+ }
+
+ // localFlags are local to the rootCmd
+ localFlags := cmd.LocalFlags()
+ for _, a := range localAliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ localFlags.StringVarP(p, a, a, value, aliasesUsage)
+ } else {
+ localFlags.StringVar(p, a, value, aliasesUsage)
+ }
+ }
+
+ // persistentFlags cannot redefine alias already used in subcommands
+ persistentFlags := cmd.PersistentFlags()
+ persistentFlags.StringVar(p, name, value, usage)
+ for _, a := range persistentAliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ persistentFlags.StringVarP(p, a, a, value, aliasesUsage)
+ } else {
+ persistentFlags.StringVar(p, a, value, aliasesUsage)
+ }
+ }
+}
+
+// AddPersistentBoolFlag is similar to AddBoolFlag but persistent.
+// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
+func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) {
+ if env != "" {
+ usage = fmt.Sprintf("%s [$%s]", usage, env)
+ }
+ if envV, ok := os.LookupEnv(env); ok {
+ var err error
+ value, err = strconv.ParseBool(envV)
+ if err != nil {
+ log.L.WithError(err).Warnf("Invalid boolean value for `%s`", env)
+ }
+ }
+ aliasesUsage := fmt.Sprintf("Alias of --%s", name)
+ p := new(bool)
+ flags := cmd.Flags()
+ for _, a := range nonPersistentAliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ flags.BoolVarP(p, a, a, value, aliasesUsage)
+ } else {
+ flags.BoolVar(p, a, value, aliasesUsage)
+ }
+ }
+
+ persistentFlags := cmd.PersistentFlags()
+ persistentFlags.BoolVar(p, name, value, usage)
+ for _, a := range aliases {
+ if len(a) == 1 {
+ // pflag doesn't support short-only flags, so we have to register long one as well here
+ persistentFlags.BoolVarP(p, a, a, value, aliasesUsage)
+ } else {
+ persistentFlags.BoolVar(p, a, value, aliasesUsage)
+ }
+ }
+}
diff --git a/cmd/nerdctl/helpers/consts.go b/cmd/nerdctl/helpers/consts.go
new file mode 100644
index 00000000000..ff1cc06ad99
--- /dev/null
+++ b/cmd/nerdctl/helpers/consts.go
@@ -0,0 +1,22 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package helpers
+
+const (
+ Category = "category"
+ Management = "management"
+)
diff --git a/cmd/nerdctl/flagutil.go b/cmd/nerdctl/helpers/flagutil.go
similarity index 79%
rename from cmd/nerdctl/flagutil.go
rename to cmd/nerdctl/helpers/flagutil.go
index 2c6102a5401..871b018a024 100644
--- a/cmd/nerdctl/flagutil.go
+++ b/cmd/nerdctl/helpers/flagutil.go
@@ -14,27 +14,17 @@
limitations under the License.
*/
-package main
+package helpers
import (
- "github.com/containerd/nerdctl/pkg/api/types"
+ "fmt"
+
"github.com/spf13/cobra"
-)
-func processImageSignOptions(cmd *cobra.Command) (opt types.ImageSignOptions, err error) {
- if opt.Provider, err = cmd.Flags().GetString("sign"); err != nil {
- return
- }
- if opt.CosignKey, err = cmd.Flags().GetString("cosign-key"); err != nil {
- return
- }
- if opt.NotationKeyName, err = cmd.Flags().GetString("notation-key-name"); err != nil {
- return
- }
- return
-}
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+)
-func processImageVerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions, err error) {
+func ProcessImageVerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions, err error) {
if opt.Provider, err = cmd.Flags().GetString("verify"); err != nil {
return
}
@@ -56,7 +46,7 @@ func processImageVerifyOptions(cmd *cobra.Command) (opt types.ImageVerifyOptions
return
}
-func processRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) {
+func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) {
debug, err := cmd.Flags().GetBool("debug")
if err != nil {
return types.GlobalCommandOptions{}, err
@@ -109,6 +99,14 @@ func processRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
if err != nil {
return types.GlobalCommandOptions{}, err
}
+ bridgeIP, err := cmd.Flags().GetString("bridge-ip")
+ if err != nil {
+ return types.GlobalCommandOptions{}, err
+ }
+ kubeHideDupe, err := cmd.Flags().GetBool("kube-hide-dupe")
+ if err != nil {
+ return types.GlobalCommandOptions{}, err
+ }
return types.GlobalCommandOptions{
Debug: debug,
DebugFull: debugFull,
@@ -123,5 +121,20 @@ func processRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error)
HostsDir: hostsDir,
Experimental: experimental,
HostGatewayIP: hostGatewayIP,
+ BridgeIP: bridgeIP,
+ KubeHideDupe: kubeHideDupe,
}, nil
}
+
+func CheckExperimental(feature string) func(cmd *cobra.Command, args []string) error {
+ return func(cmd *cobra.Command, args []string) error {
+ globalOptions, err := ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return err
+ }
+ if !globalOptions.Experimental {
+ return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature)
+ }
+ return nil
+ }
+}
diff --git a/cmd/nerdctl/completion_linux.go b/cmd/nerdctl/helpers/prompt.go
similarity index 62%
rename from cmd/nerdctl/completion_linux.go
rename to cmd/nerdctl/helpers/prompt.go
index e89d5e40960..9254699f7a6 100644
--- a/cmd/nerdctl/completion_linux.go
+++ b/cmd/nerdctl/helpers/prompt.go
@@ -14,21 +14,26 @@
limitations under the License.
*/
-package main
+package helpers
import (
- "github.com/containerd/nerdctl/pkg/apparmorutil"
+ "fmt"
+ "strings"
+
"github.com/spf13/cobra"
)
-func shellCompleteApparmorProfiles(cmd *cobra.Command) ([]string, cobra.ShellCompDirective) {
- profiles, err := apparmorutil.Profiles()
+func Confirm(cmd *cobra.Command, message string) (bool, error) {
+ message += "\nAre you sure you want to continue? [y/N] "
+ _, err := fmt.Fprint(cmd.OutOrStdout(), message)
if err != nil {
- return nil, cobra.ShellCompDirectiveError
+ return false, err
}
- var names []string // nolint: prealloc
- for _, f := range profiles {
- names = append(names, f.Name)
+
+ var confirm string
+ _, err = fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm)
+ if err != nil {
+ return false, err
}
- return names, cobra.ShellCompDirectiveNoFileComp
+ return strings.ToLower(confirm) == "y", err
}
diff --git a/cmd/nerdctl/helpers/testing.go b/cmd/nerdctl/helpers/testing.go
new file mode 100644
index 00000000000..c7f460b19a0
--- /dev/null
+++ b/cmd/nerdctl/helpers/testing.go
@@ -0,0 +1,137 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package helpers
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+
+ "github.com/containerd/nerdctl/v2/pkg/buildkitutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func CreateBuildContext(t *testing.T, dockerfile string) string {
+ tmpDir := t.TempDir()
+ err := os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfile), 0644)
+ assert.NilError(t, err)
+ return tmpDir
+}
+
+func RmiAll(base *testutil.Base) {
+ base.T.Logf("Pruning images")
+ imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines()
+ // remove empty output line at the end
+ imageIDs = imageIDs[:len(imageIDs)-1]
+ // use `Run` on purpose (same below) because `rmi all` may fail on individual
+ // image id that has an expected running container (e.g. a registry)
+ base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run()
+
+ base.T.Logf("Pruning build caches")
+ if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil {
+ base.Cmd("builder", "prune", "--force").AssertOK()
+ }
+
+ // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs
+ // https://github.com/containerd/nerdctl/pull/1833
+ if base.Target == testutil.Nerdctl {
+ base.T.Logf("Pruning all content blobs")
+ addr := base.ContainerdAddress()
+ client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace))
+ assert.NilError(base.T, err)
+ cs := client.ContentStore()
+ ctx := context.TODO()
+ wf := func(info content.Info) error {
+ base.T.Logf("Pruning blob %+v", info)
+ if err := cs.Delete(ctx, info.Digest); err != nil {
+ base.T.Log(err)
+ }
+ return nil
+ }
+ if err := cs.Walk(ctx, wf); err != nil {
+ base.T.Log(err)
+ }
+
+ base.T.Logf("Pruning all images (again?)")
+ imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines()
+ base.T.Logf("pruning following images: %+v", imageIDs)
+ base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run()
+ }
+}
+
+func ExtractDockerArchive(archiveTarPath, rootfsPath string) error {
+ if err := os.MkdirAll(rootfsPath, 0755); err != nil {
+ return err
+ }
+ workDir, err := os.MkdirTemp("", "extract-docker-archive")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(workDir)
+ if err := ExtractTarFile(workDir, archiveTarPath); err != nil {
+ return err
+ }
+ manifestJSONPath := filepath.Join(workDir, "manifest.json")
+ manifestJSONBytes, err := os.ReadFile(manifestJSONPath)
+ if err != nil {
+ return err
+ }
+ var mani DockerArchiveManifestJSON
+ if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil {
+ return err
+ }
+ if len(mani) > 1 {
+ return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani))
+ }
+ if len(mani) < 1 {
+ return errors.New("invalid archive")
+ }
+ ent := mani[0]
+ for _, l := range ent.Layers {
+ layerTarPath := filepath.Join(workDir, l)
+ if err := ExtractTarFile(rootfsPath, layerTarPath); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry
+
+type DockerArchiveManifestJSONEntry struct {
+ Config string
+ RepoTags []string
+ Layers []string
+}
+
+func ExtractTarFile(dirPath, tarFilePath string) error {
+ cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+ }
+ return nil
+}
diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go
new file mode 100644
index 00000000000..c50e16ae06c
--- /dev/null
+++ b/cmd/nerdctl/helpers/testing_linux.go
@@ -0,0 +1,182 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package helpers
+
+import (
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+)
+
+func FindIPv6(output string) net.IP {
+ var ipv6 string
+ lines := strings.Split(output, "\n")
+ for _, line := range lines {
+ if strings.Contains(line, "inet6") {
+ fields := strings.Fields(line)
+ if len(fields) > 1 {
+ ipv6 = strings.Split(fields[1], "/")[0]
+ break
+ }
+ }
+ }
+ return net.ParseIP(ipv6)
+}
+
+func RequiresStargz(base *testutil.Base) {
+ info := base.Info()
+ for _, p := range info.Plugins.Storage {
+ if p == "stargz" {
+ return
+ }
+ }
+ base.T.Skip("test requires stargz")
+}
+
+type JweKeyPair struct {
+ Prv string
+ Pub string
+ Cleanup func()
+}
+
+func NewJWEKeyPair(t testing.TB) *JweKeyPair {
+ testutil.RequireExecutable(t, "openssl")
+ td, err := os.MkdirTemp(t.TempDir(), "jwe-key-pair")
+ assert.NilError(t, err)
+ prv := filepath.Join(td, "mykey.pem")
+ pub := filepath.Join(td, "mypubkey.pem")
+ cmds := [][]string{
+ // Exec openssl commands to ensure that nerdctl is compatible with the output of openssl commands.
+ // Do NOT refactor this function to use "crypto/rsa" stdlib.
+ {"openssl", "genrsa", "-out", prv},
+ {"openssl", "rsa", "-in", prv, "-pubout", "-out", pub},
+ }
+ for _, f := range cmds {
+ cmd := exec.Command(f[0], f[1:]...)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out))
+ }
+ }
+ return &JweKeyPair{
+ Prv: prv,
+ Pub: pub,
+ Cleanup: func() {
+ _ = os.RemoveAll(td)
+ },
+ }
+}
+
+func RequiresSoci(base *testutil.Base) {
+ info := base.Info()
+ for _, p := range info.Plugins.Storage {
+ if p == "soci" {
+ return
+ }
+ }
+ base.T.Skip("test requires soci")
+}
+
+type CosignKeyPair struct {
+ PublicKey string
+ PrivateKey string
+ Cleanup func()
+}
+
+func NewCosignKeyPair(t testing.TB, path string, password string) *CosignKeyPair {
+ td, err := os.MkdirTemp(t.TempDir(), path)
+ assert.NilError(t, err)
+
+ cmd := exec.Command("cosign", "generate-key-pair")
+ cmd.Dir = td
+ cmd.Env = append(cmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", password))
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out))
+ }
+
+ publicKey := filepath.Join(td, "cosign.pub")
+ privateKey := filepath.Join(td, "cosign.key")
+
+ return &CosignKeyPair{
+ PublicKey: publicKey,
+ PrivateKey: privateKey,
+ Cleanup: func() {
+ _ = os.RemoveAll(td)
+ },
+ }
+}
+
+func ComposeUp(t *testing.T, base *testutil.Base, dockerComposeYAML string, opts ...string) {
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ projectName := comp.ProjectName()
+ t.Logf("projectName=%q", projectName)
+
+ base.ComposeCmd(append(append([]string{"-f", comp.YAMLFullPath()}, opts...), "up", "-d")...).AssertOK()
+ defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
+ base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertOK()
+ base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertOK()
+
+ checkWordpress := func() error {
+ resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 10, false)
+ if err != nil {
+ return err
+ }
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) {
+ t.Logf("respBody=%q", respBody)
+ return fmt.Errorf("respBody does not contain %q", testutil.WordpressIndexHTMLSnippet)
+ }
+ return nil
+ }
+
+ var wordpressWorking bool
+ for i := 0; i < 30; i++ {
+ t.Logf("(retry %d)", i)
+ err := checkWordpress()
+ if err == nil {
+ wordpressWorking = true
+ break
+ }
+ // NOTE: "Error establishing a database connection
" is expected for the first few iterations
+ t.Log(err)
+ time.Sleep(3 * time.Second)
+ }
+
+ if !wordpressWorking {
+ t.Fatal("wordpress is not working")
+ }
+ t.Log("wordpress seems functional")
+
+ base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK()
+ base.Cmd("volume", "inspect", fmt.Sprintf("%s_db", projectName)).AssertFail()
+ base.Cmd("network", "inspect", fmt.Sprintf("%s_default", projectName)).AssertFail()
+}
diff --git a/cmd/nerdctl/image.go b/cmd/nerdctl/image/image.go
similarity index 71%
rename from cmd/nerdctl/image.go
rename to cmd/nerdctl/image/image.go
index afae1936335..3c4bc48eeb3 100644
--- a/cmd/nerdctl/image.go
+++ b/cmd/nerdctl/image/image.go
@@ -14,31 +14,34 @@
limitations under the License.
*/
-package main
+package image
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newImageCommand() *cobra.Command {
+func NewImageCommand() *cobra.Command {
cmd := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "image",
Short: "Manage images",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
cmd.AddCommand(
- newBuildCommand(),
+ builder.NewBuildCommand(),
// commitCommand is in "container", not in "image"
imageLsCommand(),
- newHistoryCommand(),
- newPullCommand(),
- newPushCommand(),
- newLoadCommand(),
- newSaveCommand(),
- newTagCommand(),
+ NewHistoryCommand(),
+ NewPullCommand(),
+ NewPushCommand(),
+ NewLoadCommand(),
+ NewSaveCommand(),
+ NewTagCommand(),
imageRmCommand(),
newImageConvertCommand(),
newImageInspectCommand(),
@@ -50,14 +53,14 @@ func newImageCommand() *cobra.Command {
}
func imageLsCommand() *cobra.Command {
- x := newImagesCommand()
+ x := NewImagesCommand()
x.Use = "ls"
x.Aliases = []string{"list"}
return x
}
func imageRmCommand() *cobra.Command {
- x := newRmiCommand()
+ x := NewRmiCommand()
x.Use = "rm"
x.Aliases = []string{"remove"}
return x
diff --git a/cmd/nerdctl/image_convert.go b/cmd/nerdctl/image/image_convert.go
similarity index 90%
rename from cmd/nerdctl/image_convert.go
rename to cmd/nerdctl/image/image_convert.go
index b65565fae26..5b181580da6 100644
--- a/cmd/nerdctl/image_convert.go
+++ b/cmd/nerdctl/image/image_convert.go
@@ -14,15 +14,18 @@
limitations under the License.
*/
-package main
+package image
import (
"compress/gzip"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
const imageConvertHelp = `Convert an image format.
@@ -60,6 +63,11 @@ func newImageConvertCommand() *cobra.Command {
imageConvertCommand.Flags().Bool("estargz-keep-diff-id", false, "Convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc')")
// #endregion
+ // #region zstd flags
+ imageConvertCommand.Flags().Bool("zstd", false, "Convert legacy tar(.gz) layers to zstd. Should be used in conjunction with '--oci'")
+ imageConvertCommand.Flags().Int("zstd-compression-level", 3, "zstd compression level")
+ // #endregion
+
// #region zstd:chunked flags
imageConvertCommand.Flags().Bool("zstdchunked", false, "Convert legacy tar(.gz) layers to zstd:chunked for lazy pulling. Should be used in conjunction with '--oci'")
imageConvertCommand.Flags().String("zstdchunked-record-in", "", "Read 'ctr-remote optimize --record-out=' record file (EXPERIMENTAL)")
@@ -89,7 +97,7 @@ func newImageConvertCommand() *cobra.Command {
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
imageConvertCommand.Flags().StringSlice("platform", []string{}, "Convert content for a specific platform")
- imageConvertCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ imageConvertCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
imageConvertCommand.Flags().Bool("all-platforms", false, "Convert content for all platforms")
// #endregion
@@ -97,7 +105,7 @@ func newImageConvertCommand() *cobra.Command {
}
func processImageConvertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageConvertOptions{}, err
}
@@ -137,6 +145,17 @@ func processImageConvertOptions(cmd *cobra.Command) (types.ImageConvertOptions,
}
// #endregion
+ // #region zstd flags
+ zstd, err := cmd.Flags().GetBool("zstd")
+ if err != nil {
+ return types.ImageConvertOptions{}, err
+ }
+ zstdCompressionLevel, err := cmd.Flags().GetInt("zstd-compression-level")
+ if err != nil {
+ return types.ImageConvertOptions{}, err
+ }
+ // #endregion
+
// #region zstd:chunked flags
zstdchunked, err := cmd.Flags().GetBool("zstdchunked")
if err != nil {
@@ -227,6 +246,10 @@ func processImageConvertOptions(cmd *cobra.Command) (types.ImageConvertOptions,
EstargzExternalToc: estargzExternalTOC,
EstargzKeepDiffID: estargzKeepDiffID,
// #endregion
+ // #region zstd flags
+ Zstd: zstd,
+ ZstdCompressionLevel: zstdCompressionLevel,
+ // #endregion
// #region zstd:chunked flags
ZstdChunked: zstdchunked,
ZstdChunkedCompressionLevel: zstdChunkedCompressionLevel,
@@ -276,5 +299,5 @@ func imageConvertAction(cmd *cobra.Command, args []string) error {
func imageConvertShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go
new file mode 100644
index 00000000000..6968e5ab7a5
--- /dev/null
+++ b/cmd/nerdctl/image/image_convert_linux_test.go
@@ -0,0 +1,143 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
+)
+
+func TestImageConvert(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ // FIXME: windows does not support stargz
+ test.Not(test.Windows),
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "esgz",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "convert", "--oci", "--estargz",
+ testutil.CommonImage, data.Identifier("converted-image"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "nydus",
+ Require: test.Require(
+ test.Binary("nydus-image"),
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "convert", "--oci", "--nydus",
+ testutil.CommonImage, data.Identifier("converted-image"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "zstd",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3",
+ testutil.CommonImage, data.Identifier("converted-image"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "zstdchunked",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3",
+ testutil.CommonImage, data.Identifier("converted-image"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+
+}
+
+func TestImageConvertNydusVerify(t *testing.T) {
+ nerdtest.Setup()
+
+ const remoteImageKey = "remoteImageKey"
+
+ var registry *testregistry.RegistryServer
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Linux,
+ test.Binary("nydus-image"),
+ test.Binary("nydusify"),
+ test.Binary("nydusd"),
+ test.Not(nerdtest.Docker),
+ nerdtest.Rootful,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ base := testutil.NewBase(t)
+ registry = testregistry.NewWithNoAuth(base, 0, false)
+ data.Set(remoteImageKey, fmt.Sprintf("%s:%d/nydusd-image:test", "localhost", registry.Port))
+ helpers.Ensure("image", "convert", "--nydus", "--oci", testutil.CommonImage, data.Identifier("converted-image"))
+ helpers.Ensure("tag", data.Identifier("converted-image"), data.Get(remoteImageKey))
+ helpers.Ensure("push", data.Get(remoteImageKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
+ if registry != nil {
+ registry.Cleanup(nil)
+ helpers.Anyhow("rmi", "-f", data.Get(remoteImageKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("nydusify",
+ "check",
+ "--source",
+ testutil.CommonImage,
+ "--target",
+ data.Get(remoteImageKey),
+ "--source-insecure",
+ "--target-insecure",
+ )
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_cryptutil.go b/cmd/nerdctl/image/image_cryptutil.go
similarity index 90%
rename from cmd/nerdctl/image_cryptutil.go
rename to cmd/nerdctl/image/image_cryptutil.go
index 039f7720205..0268bddb001 100644
--- a/cmd/nerdctl/image_cryptutil.go
+++ b/cmd/nerdctl/image/image_cryptutil.go
@@ -14,13 +14,16 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
// registerImgcryptFlags register flags that correspond to parseImgcryptFlags().
@@ -35,7 +38,7 @@ func registerImgcryptFlags(cmd *cobra.Command, encrypt bool) {
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
flags.StringSlice("platform", []string{}, "Convert content for a specific platform")
- cmd.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
flags.Bool("all-platforms", false, "Convert content for all platforms")
// #endregion
@@ -53,7 +56,7 @@ func registerImgcryptFlags(cmd *cobra.Command, encrypt bool) {
}
func processImgCryptOptions(cmd *cobra.Command, args []string, encrypt bool) (types.ImageCryptOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageCryptOptions{}, err
}
@@ -122,5 +125,5 @@ func getImgcryptAction(encrypt bool) func(cmd *cobra.Command, args []string) err
func imgcryptShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image_decrypt.go b/cmd/nerdctl/image/image_decrypt.go
similarity index 99%
rename from cmd/nerdctl/image_decrypt.go
rename to cmd/nerdctl/image/image_decrypt.go
index fa6faf46629..6830d137d73 100644
--- a/cmd/nerdctl/image_decrypt.go
+++ b/cmd/nerdctl/image/image_decrypt.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package image
import (
"github.com/spf13/cobra"
diff --git a/cmd/nerdctl/image_encrypt.go b/cmd/nerdctl/image/image_encrypt.go
similarity index 99%
rename from cmd/nerdctl/image_encrypt.go
rename to cmd/nerdctl/image/image_encrypt.go
index 39ee30659f3..7c2ef199cbc 100644
--- a/cmd/nerdctl/image_encrypt.go
+++ b/cmd/nerdctl/image/image_encrypt.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package image
import (
"github.com/spf13/cobra"
diff --git a/cmd/nerdctl/image/image_encrypt_linux_test.go b/cmd/nerdctl/image/image_encrypt_linux_test.go
new file mode 100644
index 00000000000..3e2e93ac29e
--- /dev/null
+++ b/cmd/nerdctl/image/image_encrypt_linux_test.go
@@ -0,0 +1,82 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
+)
+
+func TestImageEncryptJWE(t *testing.T) {
+ nerdtest.Setup()
+
+ var registry *testregistry.RegistryServer
+ var keyPair *testhelpers.JweKeyPair
+
+ const remoteImageKey = "remoteImageKey"
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ // This test needs to rmi the common image
+ nerdtest.Private,
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if registry != nil {
+ registry.Cleanup(nil)
+ keyPair.Cleanup()
+ helpers.Anyhow("rmi", "-f", data.Get(remoteImageKey))
+ }
+ helpers.Anyhow("rmi", "-f", data.Identifier("decrypted"))
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ base := testutil.NewBase(t)
+ registry = testregistry.NewWithNoAuth(base, 0, false)
+ keyPair = testhelpers.NewJWEKeyPair(t)
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", registry.Port, data.Identifier())
+ helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, testutil.CommonImage, encryptImageRef)
+ inspector := helpers.Capture("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef)
+ assert.Equal(t, inspector, "1\n")
+ inspector = helpers.Capture("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef)
+ assert.Assert(t, strings.Contains(inspector, "org.opencontainers.image.enc.keys.jwe"))
+ helpers.Ensure("push", encryptImageRef)
+ helpers.Anyhow("rmi", "-f", encryptImageRef)
+ helpers.Anyhow("rmi", "-f", testutil.CommonImage)
+ data.Set(remoteImageKey, encryptImageRef)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ helpers.Fail("pull", data.Get(remoteImageKey))
+ helpers.Ensure("pull", "--quiet", "--unpack=false", data.Get(remoteImageKey))
+ helpers.Fail("image", "decrypt", "--key="+keyPair.Pub, data.Get(remoteImageKey), data.Identifier("decrypted")) // decryption needs prv key, not pub key
+ return helpers.Command("image", "decrypt", "--key="+keyPair.Prv, data.Get(remoteImageKey), data.Identifier("decrypted"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_history.go b/cmd/nerdctl/image/image_history.go
similarity index 69%
rename from cmd/nerdctl/image_history.go
rename to cmd/nerdctl/image/image_history.go
index 2550b19ab66..d4f4aa9bcc8 100644
--- a/cmd/nerdctl/image_history.go
+++ b/cmd/nerdctl/image/image_history.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package image
import (
"bytes"
@@ -23,26 +23,31 @@ import (
"fmt"
"io"
"os"
+ "strconv"
"text/tabwriter"
"text/template"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/imgutil"
+ "github.com/docker/go-units"
"github.com/opencontainers/image-spec/identity"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
)
-func newHistoryCommand() *cobra.Command {
+func NewHistoryCommand() *cobra.Command {
var historyCommand = &cobra.Command{
Use: "history [flags] IMAGE",
Short: "Show the history of an image",
- Args: IsExactArgs(1),
+ Args: helpers.IsExactArgs(1),
RunE: historyAction,
ValidArgsFunction: historyShellComplete,
SilenceUsage: true,
@@ -58,11 +63,16 @@ func addHistoryFlags(cmd *cobra.Command) {
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
})
cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs")
+ cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format (default true)")
cmd.Flags().Bool("no-trunc", false, "Don't truncate output")
}
type historyPrintable struct {
+ creationTime *time.Time
+ size int64
+
Snapshot string
+ CreatedAt string
CreatedSince string
CreatedBy string
Size string
@@ -70,7 +80,7 @@ type historyPrintable struct {
}
func historyAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -101,7 +111,7 @@ func historyAction(cmd *cobra.Command, args []string) error {
}
var historys []historyPrintable
for _, h := range configHistories {
- var size string
+ var size int64
var snapshotName string
if !h.EmptyLayer {
if len(diffIDs) <= layerCounter {
@@ -119,18 +129,18 @@ func historyAction(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("failed to get usage: %w", err)
}
- size = progress.Bytes(use.Size).String()
+ size = use.Size
snapshotName = stat.Name
layerCounter++
} else {
- size = progress.Bytes(0).String()
+ size = 0
snapshotName = ""
}
history := historyPrintable{
+ creationTime: h.Created,
+ size: size,
Snapshot: snapshotName,
- CreatedSince: formatter.TimeSinceInHuman(*h.Created),
CreatedBy: h.CreatedBy,
- Size: size,
Comment: h.Comment,
}
historys = append(historys, history)
@@ -147,9 +157,9 @@ func historyAction(cmd *cobra.Command, args []string) error {
}
type historyPrinter struct {
- w io.Writer
- quiet, noTrunc bool
- tmpl *template.Template
+ w io.Writer
+ quiet, noTrunc, human bool
+ tmpl *template.Template
}
func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
@@ -161,6 +171,11 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
if err != nil {
return err
}
+ human, err := cmd.Flags().GetBool("human")
+ if err != nil {
+ return err
+ }
+
var w io.Writer
w = os.Stdout
@@ -179,9 +194,7 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
case "raw":
return errors.New("unsupported format: \"raw\"")
default:
- if quiet {
- return errors.New("format and quiet must not be specified together")
- }
+ quiet = false
var err error
tmpl, err = formatter.ParseTemplate(format)
if err != nil {
@@ -193,12 +206,13 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
w: w,
quiet: quiet,
noTrunc: noTrunc,
+ human: human,
tmpl: tmpl,
}
for index := len(historys) - 1; index >= 0; index-- {
if err := printer.printHistory(historys[index]); err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
}
}
@@ -208,31 +222,47 @@ func printHistory(cmd *cobra.Command, historys []historyPrintable) error {
return nil
}
-func (x *historyPrinter) printHistory(p historyPrintable) error {
+func (x *historyPrinter) printHistory(printable historyPrintable) error {
+ // Truncate long values unless --no-trunc is passed
if !x.noTrunc {
- if len(p.CreatedBy) > 45 {
- p.CreatedBy = p.CreatedBy[0:44] + "…"
+ if len(printable.CreatedBy) > 45 {
+ printable.CreatedBy = printable.CreatedBy[0:44] + "…"
+ }
+ // Do not truncate snapshot id if quiet is being passed
+ if !x.quiet && len(printable.Snapshot) > 45 {
+ printable.Snapshot = printable.Snapshot[0:44] + "…"
}
}
+
+ // Format date and size for display based on --human preference
+ printable.CreatedAt = printable.creationTime.Local().Format(time.RFC3339)
+ if x.human {
+ printable.CreatedSince = formatter.TimeSinceInHuman(*printable.creationTime)
+ printable.Size = units.HumanSize(float64(printable.size))
+ } else {
+ printable.CreatedSince = printable.CreatedAt
+ printable.Size = strconv.FormatInt(printable.size, 10)
+ }
+
if x.tmpl != nil {
var b bytes.Buffer
- if err := x.tmpl.Execute(&b, p); err != nil {
+ if err := x.tmpl.Execute(&b, printable); err != nil {
return err
}
- if _, err := fmt.Fprintf(x.w, b.String()+"\n"); err != nil {
+ if _, err := fmt.Fprintln(x.w, b.String()); err != nil {
return err
}
} else if x.quiet {
- if _, err := fmt.Fprintf(x.w, "%s\n", p.Snapshot); err != nil {
+ if _, err := fmt.Fprintln(x.w, printable.Snapshot); err != nil {
return err
}
} else {
if _, err := fmt.Fprintf(x.w, "%s\t%s\t%s\t%s\t%s\n",
- p.Snapshot,
- p.CreatedSince,
- p.CreatedBy,
- p.Size,
- p.Comment,
+ printable.Snapshot,
+ printable.CreatedSince,
+ printable.CreatedBy,
+ printable.Size,
+ printable.Comment,
); err != nil {
return err
}
@@ -242,5 +272,5 @@ func (x *historyPrinter) printHistory(p historyPrintable) error {
func historyShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go
new file mode 100644
index 00000000000..9520e72535b
--- /dev/null
+++ b/cmd/nerdctl/image/image_history_test.go
@@ -0,0 +1,155 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "encoding/json"
+ "errors"
+ "io"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+type historyObj struct {
+ Snapshot string
+ CreatedAt string
+ CreatedSince string
+ CreatedBy string
+ Size string
+ Comment string
+}
+
+func decode(stdout string) ([]historyObj, error) {
+ dec := json.NewDecoder(strings.NewReader(stdout))
+ object := []historyObj{}
+ for {
+ var v historyObj
+ if err := dec.Decode(&v); err == io.EOF {
+ break
+ } else if err != nil {
+ return nil, errors.New("failed to decode history object")
+ }
+ object = append(object, v)
+ }
+
+ return object, nil
+}
+
+func TestImageHistory(t *testing.T) {
+ // Here are the current issues with regard to docker true compatibility:
+ // - we have a different definition of what a layer id is (snapshot vs. id)
+ // this will require indepth convergence when moby will handle multi-platform images
+ // - our definition of size is different
+ // this requires some investigation to figure out why it differs
+ // possibly one is unpacked on the filessystem while the other is the tar file size?
+ // - we do not truncate ids when --quiet has been provided
+ // this is a conscious decision here - truncating with --quiet does not make much sense
+
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ // XXX the results here are obviously platform dependent - and it seems like windows cannot pull a linux image?
+ test.Not(test.Windows),
+ // XXX Currently, history does not work on non-native platform, so, we cannot test reliably on other platforms
+ test.Arm64,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ // XXX: despite efforts to isolate this test, it keeps on having side effects linked to
+ // https://github.com/containerd/nerdctl/issues/3512
+ // Isolating it into a completely different root is the last ditched attempt at avoiding the issue
+ helpers.Write(nerdtest.DataRoot, test.ConfigValue(data.TempDir()))
+ helpers.Ensure("pull", "--quiet", "--platform", "linux/arm64", testutil.CommonImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "trunc, no quiet, human",
+ Command: test.Command("image", "history", "--human=true", "--format=json", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ history, err := decode(stdout)
+ assert.NilError(t, err, info)
+ assert.Equal(t, len(history), 2, info)
+ assert.Equal(t, history[0].Size, "0B", info)
+ // FIXME: how is this going to age?
+ assert.Equal(t, history[0].CreatedSince, "3 years ago", info)
+ assert.Equal(t, history[0].Snapshot, "", info)
+ assert.Equal(t, history[0].Comment, "", info)
+
+ localTimeL1, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:23-07:00")
+ localTimeL2, _ := time.Parse(time.RFC3339, "2021-03-31T10:21:21-07:00")
+ compTime1, _ := time.Parse(time.RFC3339, history[0].CreatedAt)
+ compTime2, _ := time.Parse(time.RFC3339, history[1].CreatedAt)
+ assert.Equal(t, compTime1.UTC().String(), localTimeL1.UTC().String(), info)
+ assert.Equal(t, history[0].CreatedBy, "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", info)
+ assert.Equal(t, compTime2.UTC().String(), localTimeL2.UTC().String(), info)
+ assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5…", info)
+
+ assert.Equal(t, history[1].Size, "5.947MB", info)
+ assert.Equal(t, history[1].CreatedSince, "3 years ago", info)
+ assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info)
+ assert.Equal(t, history[1].Comment, "", info)
+ }),
+ },
+ {
+ Description: "no human - dates and sizes and not prettyfied",
+ Command: test.Command("image", "history", "--human=false", "--format=json", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ history, err := decode(stdout)
+ assert.NilError(t, err, info)
+ assert.Equal(t, history[0].Size, "0", info)
+ assert.Equal(t, history[0].CreatedSince, history[0].CreatedAt, info)
+ assert.Equal(t, history[1].Size, "5947392", info)
+ assert.Equal(t, history[1].CreatedSince, history[1].CreatedAt, info)
+ }),
+ },
+ {
+ Description: "no trunc - do not truncate sha or cmd",
+ Command: test.Command("image", "history", "--human=false", "--no-trunc", "--format=json", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ history, err := decode(stdout)
+ assert.NilError(t, err, info)
+ assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a")
+ assert.Equal(t, history[1].CreatedBy, "/bin/sh -c #(nop) ADD file:3b16ffee2b26d8af5db152fcc582aaccd9e1ec9e3343874e9969a205550fe07d in / ")
+ }),
+ },
+ {
+ Description: "Quiet has no effect with format, so, go no-json, no-trunc",
+ Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n")
+ }),
+ },
+ {
+ Description: "With quiet, trunc has no effect",
+ Command: test.Command("image", "history", "--human=false", "--no-trunc", "--quiet", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ assert.Equal(t, stdout, "\nsha256:56bf55b8eed1f0b4794a30386e4d1d3da949c25bcb5155e898097cd75dc77c2a\n")
+ }),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_inspect.go b/cmd/nerdctl/image/image_inspect.go
similarity index 84%
rename from cmd/nerdctl/image_inspect.go
rename to cmd/nerdctl/image/image_inspect.go
index a255324ba2c..c24d8cf64e3 100644
--- a/cmd/nerdctl/image_inspect.go
+++ b/cmd/nerdctl/image/image_inspect.go
@@ -14,13 +14,16 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
func newImageInspectCommand() *cobra.Command {
@@ -45,14 +48,14 @@ func newImageInspectCommand() *cobra.Command {
// #region platform flags
imageInspectCommand.Flags().String("platform", "", "Inspect a specific platform") // not a slice, and there is no --all-platforms
- imageInspectCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ imageInspectCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
// #endregion
return imageInspectCommand
}
-func processImageInspectOptions(cmd *cobra.Command, platform *string) (types.ImageInspectOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+func ProcessImageInspectOptions(cmd *cobra.Command, platform *string) (types.ImageInspectOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageInspectOptions{}, err
}
@@ -81,7 +84,7 @@ func processImageInspectOptions(cmd *cobra.Command, platform *string) (types.Ima
}
func imageInspectAction(cmd *cobra.Command, args []string) error {
- options, err := processImageInspectOptions(cmd, nil)
+ options, err := ProcessImageInspectOptions(cmd, nil)
if err != nil {
return err
}
@@ -97,5 +100,5 @@ func imageInspectAction(cmd *cobra.Command, args []string) error {
func imageInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image/image_inspect_test.go b/cmd/nerdctl/image/image_inspect_test.go
new file mode 100644
index 00000000000..808fd199d46
--- /dev/null
+++ b/cmd/nerdctl/image/image_inspect_test.go
@@ -0,0 +1,235 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "runtime"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestImageInspectSimpleCases(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.CommonImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Contains some stuff",
+ Command: test.Command("image", "inspect", testutil.CommonImage),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Assert(t, len(dc[0].RootFS.Layers) > 0, info)
+ assert.Assert(t, dc[0].Architecture != "", info)
+ assert.Assert(t, dc[0].Size > 0, info)
+ }),
+ },
+ {
+ Description: "RawFormat support (.Id)",
+ Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "typedFormat support (.ID)",
+ Command: test.Command("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "Error for image not found",
+ Command: test.Command("image", "inspect", "dne:latest", "dne2:latest"),
+ Expected: test.Expects(1, []error{
+ errors.New("no such image: dne:latest"),
+ errors.New("no such image: dne2:latest"),
+ }, nil),
+ },
+ },
+ }
+
+ if runtime.GOOS == "windows" {
+ testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524")
+ }
+
+ testCase.Run(t)
+}
+
+func TestImageInspectDifferentValidReferencesForTheSameImage(t *testing.T) {
+ nerdtest.Setup()
+
+ tags := []string{
+ "",
+ ":latest",
+ }
+ names := []string{
+ "busybox",
+ "docker.io/library/busybox",
+ "registry-1.docker.io/library/busybox",
+ }
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ // FIXME: this test depends on hub images that do not have windows versions
+ test.Not(test.Windows),
+ // We need a clean slate
+ nerdtest.Private,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", "alpine")
+ helpers.Ensure("pull", "--quiet", "busybox")
+ helpers.Ensure("pull", "--quiet", "registry-1.docker.io/library/busybox")
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "name and tags +/- sha combinations",
+ Command: test.Command("image", "inspect", "busybox"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ reference := dc[0].ID
+ sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:")
+
+ for _, name := range names {
+ for _, tag := range tags {
+ it := nerdtest.InspectImage(helpers, name+tag)
+ assert.Equal(t, it.ID, reference)
+ it = nerdtest.InspectImage(helpers, name+tag+"@sha256:"+sha)
+ assert.Equal(t, it.ID, reference)
+ }
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "by digest, short or long, with or without prefix",
+ Command: test.Command("image", "inspect", "busybox"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ reference := dc[0].ID
+ sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:")
+
+ for _, id := range []string{"sha256:" + sha, sha, sha[0:8], "sha256:" + sha[0:8]} {
+ it := nerdtest.InspectImage(helpers, id)
+ assert.Equal(t, it.ID, reference)
+ }
+
+ // Now, tag alpine with a short id
+ // Build reference values for comparison
+ alpine := nerdtest.InspectImage(helpers, "alpine")
+
+ // Demonstrate image name precedence over digest lookup
+ // Using the shortened sha should no longer get busybox, but rather the newly tagged Alpine
+ // FIXME: this is triggering https://github.com/containerd/nerdctl/issues/3016
+ // We cannot get rid of that image now, which does break local testing
+ helpers.Ensure("tag", "alpine", sha[0:8])
+ it := nerdtest.InspectImage(helpers, sha[0:8])
+ assert.Equal(t, it.ID, alpine.ID)
+ },
+ }
+ },
+ },
+ {
+ Description: "prove that wrong references with correct digest do not get resolved",
+ Command: test.Command("image", "inspect", "busybox"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ sha := strings.TrimPrefix(dc[0].RepoDigests[0], "busybox@sha256:")
+
+ for _, id := range []string{"doesnotexist", "doesnotexist:either", "busybox:bogustag"} {
+ cmd := helpers.Command("image", "inspect", id+"@sha256:"+sha)
+ cmd.Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{fmt.Errorf("no such image: %s@sha256:%s", id, sha)},
+ })
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "prove that invalid reference return no result without crashing",
+ Command: test.Command("image", "inspect", "busybox"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+
+ for _, id := range []string{"∞∞∞∞∞∞∞∞∞∞", "busybox:∞∞∞∞∞∞∞∞∞∞"} {
+ cmd := helpers.Command("image", "inspect", id)
+ cmd.Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{fmt.Errorf("invalid reference format: %s", id)},
+ })
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "retrieving multiple entries at once",
+ Command: test.Command("image", "inspect", "busybox", "busybox"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Image
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 2, len(dc), "Unexpectedly did not get 2 results\n"+info)
+ reference := nerdtest.InspectImage(helpers, "busybox")
+ assert.Equal(t, dc[0].ID, reference.ID)
+ assert.Equal(t, dc[1].ID, reference.ID)
+ },
+ }
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_list.go b/cmd/nerdctl/image/image_list.go
similarity index 81%
rename from cmd/nerdctl/image_list.go
rename to cmd/nerdctl/image/image_list.go
index e63f14fc6fd..06566f471fd 100644
--- a/cmd/nerdctl/image_list.go
+++ b/cmd/nerdctl/image/image_list.go
@@ -14,19 +14,22 @@
limitations under the License.
*/
-package main
+package image
import (
"fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
- "github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
-func newImagesCommand() *cobra.Command {
+func NewImagesCommand() *cobra.Command {
shortHelp := "List images"
longHelp := shortHelp + `
@@ -67,49 +70,47 @@ Properties:
return imagesCommand
}
-func processImageListOptions(cmd *cobra.Command, args []string) (types.ImageListOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+func processImageListOptions(cmd *cobra.Command, args []string) (*types.ImageListOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
var filters []string
-
if len(args) > 0 {
- canonicalRef, err := referenceutil.ParseAny(args[0])
+ parsedReference, err := referenceutil.Parse(args[0])
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
- filters = append(filters, fmt.Sprintf("name==%s", canonicalRef.String()))
- filters = append(filters, fmt.Sprintf("name==%s", args[0]))
+ filters = []string{fmt.Sprintf("name==%s", parsedReference)}
}
quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
noTrunc, err := cmd.Flags().GetBool("no-trunc")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
format, err := cmd.Flags().GetString("format")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
var inputFilters []string
if cmd.Flags().Changed("filter") {
inputFilters, err = cmd.Flags().GetStringSlice("filter")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
}
digests, err := cmd.Flags().GetBool("digests")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
names, err := cmd.Flags().GetBool("names")
if err != nil {
- return types.ImageListOptions{}, err
+ return nil, err
}
- return types.ImageListOptions{
+ return &types.ImageListOptions{
GOptions: globalOptions,
Quiet: quiet,
NoTrunc: noTrunc,
@@ -145,7 +146,7 @@ func imagesAction(cmd *cobra.Command, args []string) error {
func imagesShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) == 0 {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go
new file mode 100644
index 00000000000..bc88386ef4c
--- /dev/null
+++ b/cmd/nerdctl/image/image_list_test.go
@@ -0,0 +1,391 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/tabutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestImages(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "No params",
+ Command: test.Command("images"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 2, info)
+ header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE"
+ if nerdtest.IsDocker() {
+ header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE"
+ }
+ tab := tabutil.NewReader(header)
+ err := tab.ParseHeader(lines[0])
+ assert.NilError(t, err, info)
+ found := false
+ for _, line := range lines[1:] {
+ repo, _ := tab.ReadRow(line, "REPOSITORY")
+ tag, _ := tab.ReadRow(line, "TAG")
+ if repo+":"+tag == testutil.CommonImage {
+ found = true
+ break
+ }
+ }
+ assert.Assert(t, found, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "With names",
+ Command: test.Command("images", "--names", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(testutil.CommonImage),
+ func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 2, info)
+ tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE")
+ err := tab.ParseHeader(lines[0])
+ assert.NilError(t, err, info)
+ found := false
+ for _, line := range lines[1:] {
+ name, _ := tab.ReadRow(line, "NAME")
+ if name == testutil.CommonImage {
+ found = true
+ break
+ }
+ }
+
+ assert.Assert(t, found, info)
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "CheckCreatedTime",
+ Command: test.Command("images", "--format", "'{{json .CreatedAt}}'"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 2, info)
+ createdTimes := lines
+ slices.Reverse(createdTimes)
+ assert.Assert(t, slices.IsSorted(createdTimes), info)
+ },
+ }
+ },
+ },
+ },
+ }
+
+ if runtime.GOOS == "windows" {
+ testCase.Require = test.Require(
+ testCase.Require,
+ nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524"),
+ )
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagesFilter(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: nerdtest.Build,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ helpers.Ensure("tag", testutil.CommonImage, "taggedimage:one-fragment-one")
+ helpers.Ensure("tag", testutil.CommonImage, "taggedimage:two-fragment-two")
+
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"] \n
+LABEL foo=bar
+LABEL version=0.1
+RUN echo "actually creating a layer so that docker sets the createdAt time"
+`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", "taggedimage:one-fragment-one")
+ helpers.Anyhow("rmi", "-f", "taggedimage:two-fragment-two")
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ data.Set("builtImageID", data.Identifier())
+ return helpers.Command("build", "-t", data.Identifier(), data.Get("buildCtx"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ SubTests: []*test.Case{
+ {
+ Description: "label=foo=bar",
+ Command: test.Command("images", "--filter", "label=foo=bar"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "label=foo=bar1",
+ Command: test.Command("images", "--filter", "label=foo=bar1"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.DoesNotContain(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "label=foo=bar label=version=0.1",
+ Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "label=foo=bar label=version=0.2",
+ Command: test.Command("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.DoesNotContain(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "label=version",
+ Command: test.Command("images", "--filter", "label=version"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "reference=ID*",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("images", "--filter", fmt.Sprintf("reference=%s*", data.Get("builtImageID")))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("builtImageID")),
+ }
+ },
+ },
+ {
+ Description: "reference=tagged*:*fragment*",
+ Command: test.Command("images", "--filter", "reference=tagged*:*fragment*"),
+ Expected: test.Expects(0, nil, test.All(
+ test.Contains("one-"),
+ test.Contains("two-"),
+ )),
+ },
+ {
+ Description: "before=ID:latest",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("images", "--filter", fmt.Sprintf("before=%s:latest", data.Get("builtImageID")))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(testutil.ImageRepo(testutil.CommonImage)),
+ test.DoesNotContain(data.Get("builtImageID")),
+ ),
+ }
+ },
+ },
+ {
+ Description: "since=" + testutil.CommonImage,
+ Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("builtImageID")),
+ test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)),
+ ),
+ }
+ },
+ },
+ {
+ Description: "since=" + testutil.CommonImage + " " + testutil.CommonImage,
+ Command: test.Command("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain(data.Get("builtImageID")),
+ test.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)),
+ ),
+ }
+ },
+ },
+ {
+ Description: "since=non-exists-image",
+ Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3511"),
+ Command: test.Command("images", "--filter", "since=non-exists-image"),
+ Expected: test.Expects(-1, []error{errors.New("No such image: ")}, nil),
+ },
+ {
+ Description: "before=non-exists-image",
+ Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3511"),
+ Command: test.Command("images", "--filter", "before=non-exists-image"),
+ Expected: test.Expects(-1, []error{errors.New("No such image: ")}, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagesFilterDangling(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Description: "TestImagesFilterDangling",
+ // This test relies on a clean slate and the ability to GC everything
+ NoParallel: true,
+ Require: nerdtest.Build,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-notag-string"]
+ `, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ data.Set("buildCtx", buildCtx)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("container", "prune", "-f")
+ helpers.Anyhow("image", "prune", "--all", "-f")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("build", data.Get("buildCtx"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ SubTests: []*test.Case{
+ {
+ Description: "dangling",
+ Command: test.Command("images", "--filter", "dangling=true"),
+ Expected: test.Expects(0, nil, test.Contains("")),
+ },
+ {
+ Description: "not dangling",
+ Command: test.Command("images", "--filter", "dangling=false"),
+ Expected: test.Expects(0, nil, test.DoesNotContain("")),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagesKubeWithKubeHideDupe(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ nerdtest.OnlyKubernetes,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.BusyboxImage)
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "The same imageID will not print no-repo:tag in k8s.io with kube-hide-dupe",
+ Command: test.Command("--kube-hide-dupe", "images"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var imageID string
+ var skipLine int
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE"
+ if nerdtest.IsDocker() {
+ header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE"
+ }
+ tab := tabutil.NewReader(header)
+ err := tab.ParseHeader(lines[0])
+ assert.NilError(t, err, info)
+ found := true
+ for i, line := range lines[1:] {
+ repo, _ := tab.ReadRow(line, "REPOSITORY")
+ tag, _ := tab.ReadRow(line, "TAG")
+ if repo+":"+tag == testutil.BusyboxImage {
+ skipLine = i
+ imageID, _ = tab.ReadRow(line, "IMAGE ID")
+ break
+ }
+ }
+ for i, line := range lines[1:] {
+ if i == skipLine {
+ continue
+ }
+ id, _ := tab.ReadRow(line, "IMAGE ID")
+ if id == imageID {
+ found = false
+ break
+ }
+ }
+ assert.Assert(t, found, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "the same imageId will print no-repo:tag in k8s.io without kube-hide-dupe",
+ Command: test.Command("images"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(""),
+ }
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_load.go b/cmd/nerdctl/image/image_load.go
similarity index 77%
rename from cmd/nerdctl/image_load.go
rename to cmd/nerdctl/image/image_load.go
index 5d3f31063ce..3ff8b18a892 100644
--- a/cmd/nerdctl/image_load.go
+++ b/cmd/nerdctl/image/image_load.go
@@ -14,16 +14,19 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/load"
)
-func newLoadCommand() *cobra.Command {
+func NewLoadCommand() *cobra.Command {
var loadCommand = &cobra.Command{
Use: "load",
Args: cobra.NoArgs,
@@ -35,11 +38,12 @@ func newLoadCommand() *cobra.Command {
}
loadCommand.Flags().StringP("input", "i", "", "Read from tar archive file, instead of STDIN")
+ loadCommand.Flags().BoolP("quiet", "q", false, "Suppress the load output")
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
loadCommand.Flags().StringSlice("platform", []string{}, "Import content for a specific platform")
- loadCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ loadCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
loadCommand.Flags().Bool("all-platforms", false, "Import content for all platforms")
// #endregion
@@ -51,7 +55,7 @@ func processLoadCommandFlags(cmd *cobra.Command) (types.ImageLoadOptions, error)
if err != nil {
return types.ImageLoadOptions{}, err
}
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageLoadOptions{}, err
}
@@ -63,6 +67,10 @@ func processLoadCommandFlags(cmd *cobra.Command) (types.ImageLoadOptions, error)
if err != nil {
return types.ImageLoadOptions{}, err
}
+ quiet, err := cmd.Flags().GetBool("quiet")
+ if err != nil {
+ return types.ImageLoadOptions{}, err
+ }
return types.ImageLoadOptions{
GOptions: globalOptions,
Input: input,
@@ -70,6 +78,7 @@ func processLoadCommandFlags(cmd *cobra.Command) (types.ImageLoadOptions, error)
AllPlatforms: allPlatforms,
Stdout: cmd.OutOrStdout(),
Stdin: cmd.InOrStdin(),
+ Quiet: quiet,
}, nil
}
@@ -85,5 +94,6 @@ func loadAction(cmd *cobra.Command, _ []string) error {
}
defer cancel()
- return image.Load(ctx, client, options)
+ _, err = load.FromArchive(ctx, client, options)
+ return err
}
diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go
new file mode 100644
index 00000000000..4a0e170fa3e
--- /dev/null
+++ b/cmd/nerdctl/image/image_load_test.go
@@ -0,0 +1,114 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestLoadStdinFromPipe(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Description: "TestLoadStdinFromPipe",
+ Require: test.Linux,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ helpers.Ensure("tag", testutil.CommonImage, identifier)
+ helpers.Ensure("save", identifier, "-o", filepath.Join(data.TempDir(), "common.tar"))
+ helpers.Ensure("rmi", "-f", identifier)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("load")
+ reader, err := os.Open(filepath.Join(data.TempDir(), "common.tar"))
+ assert.NilError(t, err, "failed to open common.tar")
+ cmd.WithStdin(reader)
+ return cmd
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ identifier := data.Identifier()
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(fmt.Sprintf("Loaded image: %s:latest", identifier)),
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.Contains(helpers.Capture("images"), identifier))
+ },
+ ),
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestLoadStdinEmpty(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Description: "TestLoadStdinEmpty",
+ Require: test.Linux,
+ Command: test.Command("load"),
+ Expected: test.Expects(1, nil, nil),
+ }
+
+ testCase.Run(t)
+}
+
+func TestLoadQuiet(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Description: "TestLoadQuiet",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("pull", testutil.CommonImage)
+ helpers.Ensure("tag", testutil.CommonImage, identifier)
+ helpers.Ensure("save", identifier, "-o", filepath.Join(data.TempDir(), "common.tar"))
+ helpers.Ensure("rmi", "-f", identifier)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("load", "--quiet", "--input", filepath.Join(data.TempDir(), "common.tar"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(fmt.Sprintf("Loaded image: %s:latest", data.Identifier())),
+ test.DoesNotContain("Loading layer"),
+ ),
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_prune.go b/cmd/nerdctl/image/image_prune.go
similarity index 75%
rename from cmd/nerdctl/image_prune.go
rename to cmd/nerdctl/image/image_prune.go
index f444c9bb8c2..9eefa288958 100644
--- a/cmd/nerdctl/image_prune.go
+++ b/cmd/nerdctl/image/image_prune.go
@@ -14,16 +14,17 @@
limitations under the License.
*/
-package main
+package image
import (
"fmt"
- "strings"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
func newImagePruneCommand() *cobra.Command {
@@ -37,12 +38,13 @@ func newImagePruneCommand() *cobra.Command {
}
imagePruneCommand.Flags().BoolP("all", "a", false, "Remove all unused images, not just dangling ones")
+ imagePruneCommand.Flags().StringSlice("filter", []string{}, "Filter output based on conditions provided")
imagePruneCommand.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
return imagePruneCommand
}
func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImagePruneOptions{}, err
}
@@ -51,6 +53,14 @@ func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, erro
return types.ImagePruneOptions{}, err
}
+ var filters []string
+ if cmd.Flags().Changed("filter") {
+ filters, err = cmd.Flags().GetStringSlice("filter")
+ if err != nil {
+ return types.ImagePruneOptions{}, err
+ }
+ }
+
force, err := cmd.Flags().GetBool("force")
if err != nil {
return types.ImagePruneOptions{}, err
@@ -60,6 +70,7 @@ func processImagePruneOptions(cmd *cobra.Command) (types.ImagePruneOptions, erro
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
All: all,
+ Filters: filters,
Force: force,
}, err
}
@@ -71,22 +82,15 @@ func imagePruneAction(cmd *cobra.Command, _ []string) error {
}
if !options.Force {
- var (
- confirm string
- msg string
- )
+ var msg string
if !options.All {
msg = "This will remove all dangling images."
} else {
msg = "This will remove all images without at least one container associated to them."
}
- msg += "\nAre you sure you want to continue? [y/N] "
-
- fmt.Fprintf(cmd.OutOrStdout(), "WARNING! %s", msg)
- fmt.Fscanf(cmd.InOrStdin(), "%s", &confirm)
- if strings.ToLower(confirm) != "y" {
- return nil
+ if confirmed, err := helpers.Confirm(cmd, fmt.Sprintf("WARNING! %s.", msg)); err != nil || !confirmed {
+ return err
}
}
diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go
new file mode 100644
index 00000000000..8a04d24a417
--- /dev/null
+++ b/cmd/nerdctl/image/image_prune_test.go
@@ -0,0 +1,242 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestImagePrune(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // Cannot use a custom namespace with buildkitd right now, so, no parallel it is
+ testCase.NoParallel = true
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ // We need to delete everything here for prune to make any sense
+ imgList := strings.TrimSpace(helpers.Capture("images", "--no-trunc", "-aq"))
+ if imgList != "" {
+ helpers.Ensure(append([]string{"rmi", "-f"}, strings.Split(imgList, "\n")...)...)
+ }
+ }
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "without all",
+ NoParallel: true,
+ Require: test.Require(
+ // This never worked with Docker - the only reason we ever got was side effects from other tests
+ // See inline comments.
+ test.Not(nerdtest.Docker),
+ nerdtest.Build,
+ ),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ dockerfile := fmt.Sprintf(`FROM %s
+ CMD ["echo", "nerdctl-test-image-prune"]
+ `, testutil.CommonImage)
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx)
+ // After we rebuild with tag, docker will no longer show the version from above
+ // Swapping order does not change anything.
+ helpers.Ensure("build", "-t", identifier, buildCtx)
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, ""), "Missing ")
+ assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier)
+ },
+ Command: test.Command("image", "prune", "--force"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ identifier := data.Identifier()
+ return &test.Expected{
+ Output: test.All(
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, !strings.Contains(stdout, identifier), info)
+ },
+ func(stdout string, info string, t *testing.T) {
+ imgList := helpers.Capture("images")
+ assert.Assert(t, !strings.Contains(imgList, ""), imgList)
+ assert.Assert(t, strings.Contains(imgList, identifier), info)
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "with all",
+ Require: test.Require(
+ // Same as above
+ test.Not(nerdtest.Docker),
+ nerdtest.Build,
+ ),
+ // Cannot use a custom namespace with buildkitd right now, so, no parallel it is
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("rmi", "-f", identifier)
+ helpers.Anyhow("rm", "-f", identifier)
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ dockerfile := fmt.Sprintf(`FROM %s
+ CMD ["echo", "nerdctl-test-image-prune"]
+ `, testutil.CommonImage)
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", buildCtx)
+ helpers.Ensure("build", "-t", identifier, buildCtx)
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, ""), "Missing ")
+ assert.Assert(t, strings.Contains(imgList, identifier), "Missing "+identifier)
+ helpers.Ensure("run", "--name", identifier, identifier)
+ },
+ Command: test.Command("image", "prune", "--force", "--all"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info)
+ },
+ func(stdout string, info string, t *testing.T) {
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, data.Identifier()), info)
+ assert.Assert(t, !strings.Contains(imgList, ""), imgList)
+ helpers.Ensure("rm", "-f", data.Identifier())
+ removed := helpers.Capture("image", "prune", "--force", "--all")
+ assert.Assert(t, strings.Contains(removed, data.Identifier()), info)
+ imgList = helpers.Capture("images")
+ assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info)
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "with filter label",
+ Require: nerdtest.Build,
+ // Cannot use a custom namespace with buildkitd right now, so, no parallel it is
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-test-image-prune-filter-label"]
+LABEL foo=bar
+LABEL version=0.1`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", data.Identifier(), buildCtx)
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier())
+ },
+ Command: test.Command("image", "prune", "--force", "--all", "--filter", "label=foo=baz"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, !strings.Contains(stdout, data.Identifier()), info)
+ },
+ func(stdout string, info string, t *testing.T) {
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, data.Identifier()), info)
+ },
+ func(stdout string, info string, t *testing.T) {
+ prune := helpers.Capture("image", "prune", "--force", "--all", "--filter", "label=foo=bar")
+ assert.Assert(t, strings.Contains(prune, data.Identifier()), info)
+ imgList := helpers.Capture("images")
+ assert.Assert(t, !strings.Contains(imgList, data.Identifier()), info)
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "with until",
+ Require: nerdtest.Build,
+ // Cannot use a custom namespace with buildkitd right now, so, no parallel it is
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ dockerfile := fmt.Sprintf(`FROM %s
+RUN echo "Anything, so that we create actual content for docker to set the current time for CreatedAt"
+CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage)
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", data.Identifier(), buildCtx)
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, data.Identifier()), "Missing "+data.Identifier())
+ data.Set("imageID", data.Identifier())
+ },
+ Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=12h"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain(data.Get("imageID")),
+ func(stdout string, info string, t *testing.T) {
+ imgList := helpers.Capture("images")
+ assert.Assert(t, strings.Contains(imgList, data.Get("imageID")), info)
+ },
+ ),
+ }
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Wait and remove until=10ms",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ time.Sleep(1 * time.Second)
+ },
+ Command: test.Command("image", "prune", "--force", "--all", "--filter", "until=10ms"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("imageID")),
+ func(stdout string, info string, t *testing.T) {
+ imgList := helpers.Capture("images")
+ assert.Assert(t, !strings.Contains(imgList, data.Get("imageID")), imgList, info)
+ },
+ ),
+ }
+ },
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_pull.go b/cmd/nerdctl/image/image_pull.go
similarity index 74%
rename from cmd/nerdctl/image_pull.go
rename to cmd/nerdctl/image/image_pull.go
index 71d00899d23..cedef1af639 100644
--- a/cmd/nerdctl/image_pull.go
+++ b/cmd/nerdctl/image/image_pull.go
@@ -14,20 +14,25 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
-func newPullCommand() *cobra.Command {
+func NewPullCommand() *cobra.Command {
var pullCommand = &cobra.Command{
Use: "pull [flags] NAME[:TAG]",
Short: "Pull an image from a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to pull image from IPFS.",
- Args: IsExactArgs(1),
+ Args: helpers.IsExactArgs(1),
RunE: pullAction,
SilenceUsage: true,
SilenceErrors: true,
@@ -40,7 +45,7 @@ func newPullCommand() *cobra.Command {
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
pullCommand.Flags().StringSlice("platform", nil, "Pull content for a specific platform")
- pullCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ pullCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
pullCommand.Flags().Bool("all-platforms", false, "Pull content for all platforms")
// #endregion
@@ -56,6 +61,10 @@ func newPullCommand() *cobra.Command {
pullCommand.Flags().String("cosign-certificate-oidc-issuer-regexp", "", "A regular expression alternative to --certificate-oidc-issuer for --verify=cosign,. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows")
// #endregion
+ // #region socipull flags
+ pullCommand.Flags().String("soci-index-digest", "", "Specify a particular index digest for SOCI. If left empty, SOCI will automatically use the index determined by the selection policy.")
+ // #endregion
+
pullCommand.Flags().BoolP("quiet", "q", false, "Suppress verbose output")
pullCommand.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)")
@@ -64,7 +73,7 @@ func newPullCommand() *cobra.Command {
}
func processPullCommandFlags(cmd *cobra.Command) (types.ImagePullOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImagePullOptions{}, err
}
@@ -77,10 +86,20 @@ func processPullCommandFlags(cmd *cobra.Command) (types.ImagePullOptions, error)
return types.ImagePullOptions{}, err
}
+ ociSpecPlatform, err := platformutil.NewOCISpecPlatformSlice(allPlatforms, platform)
+ if err != nil {
+ return types.ImagePullOptions{}, err
+ }
+
unpackStr, err := cmd.Flags().GetString("unpack")
if err != nil {
return types.ImagePullOptions{}, err
}
+ unpack, err := strutil.ParseBoolOrAuto(unpackStr)
+ if err != nil {
+ return types.ImagePullOptions{}, err
+ }
+
quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
return types.ImagePullOptions{}, err
@@ -89,20 +108,30 @@ func processPullCommandFlags(cmd *cobra.Command) (types.ImagePullOptions, error)
if err != nil {
return types.ImagePullOptions{}, err
}
- verifyOptions, err := processImageVerifyOptions(cmd)
+
+ sociIndexDigest, err := cmd.Flags().GetString("soci-index-digest")
+ if err != nil {
+ return types.ImagePullOptions{}, err
+ }
+
+ verifyOptions, err := helpers.ProcessImageVerifyOptions(cmd)
if err != nil {
return types.ImagePullOptions{}, err
}
return types.ImagePullOptions{
- GOptions: globalOptions,
- VerifyOptions: verifyOptions,
- AllPlatforms: allPlatforms,
- Platform: platform,
- Unpack: unpackStr,
- Quiet: quiet,
- IPFSAddress: ipfsAddressStr,
- Stdout: cmd.OutOrStdout(),
- Stderr: cmd.OutOrStderr(),
+ GOptions: globalOptions,
+ VerifyOptions: verifyOptions,
+ OCISpecPlatform: ociSpecPlatform,
+ Unpack: unpack,
+ Mode: "always",
+ Quiet: quiet,
+ IPFSAddress: ipfsAddressStr,
+ RFlags: types.RemoteSnapshotterFlags{
+ SociIndexDigest: sociIndexDigest,
+ },
+ Stdout: cmd.OutOrStdout(),
+ Stderr: cmd.OutOrStderr(),
+ ProgressOutputToStdout: true,
}, nil
}
diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go
new file mode 100644
index 00000000000..c350d1e71a4
--- /dev/null
+++ b/cmd/nerdctl/image/image_pull_linux_test.go
@@ -0,0 +1,271 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
+)
+
+func TestImagePullWithCosign(t *testing.T) {
+ nerdtest.Setup()
+
+ var registry *testregistry.RegistryServer
+ var keyPair *testhelpers.CosignKeyPair
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Linux,
+ nerdtest.Build,
+ test.Binary("cosign"),
+ test.Not(nerdtest.Docker),
+ ),
+ Env: map[string]string{
+ "COSIGN_PASSWORD": "1",
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ keyPair = testhelpers.NewCosignKeyPair(t, "cosign-key-pair", "1")
+ base := testutil.NewBase(t)
+ registry = testregistry.NewWithNoAuth(base, 0, false)
+ testImageRef := fmt.Sprintf("%s:%d/%s", "127.0.0.1", registry.Port, data.Identifier())
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", testImageRef+":one", buildCtx)
+ helpers.Ensure("build", "-t", testImageRef+":two", buildCtx)
+ helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":one")
+ helpers.Ensure("push", "--sign=cosign", "--cosign-key="+keyPair.PrivateKey, testImageRef+":two")
+ helpers.Ensure("rmi", "-f", testImageRef)
+ data.Set("imageref", testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if keyPair != nil {
+ keyPair.Cleanup()
+ }
+ if registry != nil {
+ registry.Cleanup(nil)
+ testImageRef := fmt.Sprintf("%s:%d/%s", "127.0.0.1", registry.Port, data.Identifier())
+ helpers.Anyhow("rmi", "-f", testImageRef+":one")
+ helpers.Anyhow("rmi", "-f", testImageRef+":two")
+ }
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Pull with the correct key",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("pull", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, data.Get("imageref")+":one")
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "Pull with unrelated key",
+ Env: map[string]string{
+ "COSIGN_PASSWORD": "2",
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ newKeyPair := testhelpers.NewCosignKeyPair(t, "cosign-key-pair-test", "2")
+ return helpers.Command("pull", "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey, data.Get("imageref")+":two")
+ },
+ Expected: test.Expects(12, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagePullPlainHttpWithDefaultPort(t *testing.T) {
+ nerdtest.Setup()
+
+ var registry *testregistry.RegistryServer
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.Build,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ base := testutil.NewBase(t)
+ registry = testregistry.NewWithNoAuth(base, 80, false)
+ testImageRef := fmt.Sprintf("%s/%s:%s",
+ registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, testutil.CommonImage)
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+ helpers.Ensure("build", "-t", testImageRef, buildCtx)
+ helpers.Ensure("--insecure-registry", "push", testImageRef)
+ helpers.Ensure("rmi", "-f", testImageRef)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ testImageRef := fmt.Sprintf("%s/%s:%s",
+ registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ return helpers.Command("--insecure-registry", "pull", testImageRef)
+ },
+ Expected: test.Expects(0, nil, nil),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if registry != nil {
+ registry.Cleanup(nil)
+ testImageRef := fmt.Sprintf("%s/%s:%s",
+ registry.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ helpers.Anyhow("rmi", "-f", testImageRef)
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagePullSoci(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ Require: test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.Soci,
+ ),
+
+ // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount`
+ // They also feel prone to raciness...
+ SubTests: []*test.Case{
+ {
+ Description: "Run without specifying SOCI index",
+ NoParallel: true,
+ Data: test.
+ WithData("remoteSnapshotsExpectedCount", "11").
+ Set("sociIndexDigest", ""),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ cmd := helpers.Custom("mount")
+ cmd.Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge")))
+ },
+ })
+ helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("mount")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount"))
+ remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge")
+ assert.Equal(t,
+ data.Get("remoteSnapshotsExpectedCount"),
+ strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount),
+ info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Run with bad SOCI index",
+ NoParallel: true,
+ Data: test.
+ WithData("remoteSnapshotsExpectedCount", "11").
+ Set("sociIndexDigest", "sha256:thisisabadindex0000000000000000000000000000000000000000000000000"),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ cmd := helpers.Custom("mount")
+ cmd.Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ data.Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge")))
+ },
+ })
+ helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("mount")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Get("remoteSnapshotsInitialCount"))
+ remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge")
+ assert.Equal(t,
+ data.Get("remoteSnapshotsExpectedCount"),
+ strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount),
+ info)
+ },
+ }
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestImagePullProcessOutput(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ SubTests: []*test.Case{
+ {
+ Description: "Pull Image - output should be in stdout",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", testutil.BusyboxImage)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("pull", testutil.BusyboxImage)
+ },
+ Expected: test.Expects(0, nil, test.Contains(testutil.BusyboxImage)),
+ },
+ {
+ Description: "Run Container with image pull - output should be in stderr",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", testutil.BusyboxImage)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", testutil.BusyboxImage)
+ },
+ Expected: test.Expects(0, nil, test.DoesNotContain(testutil.BusyboxImage)),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_push.go b/cmd/nerdctl/image/image_push.go
similarity index 73%
rename from cmd/nerdctl/image_push.go
rename to cmd/nerdctl/image/image_push.go
index b217d281a97..eebadbf7586 100644
--- a/cmd/nerdctl/image_push.go
+++ b/cmd/nerdctl/image/image_push.go
@@ -14,24 +14,27 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
const (
allowNonDistFlag = "allow-nondistributable-artifacts"
)
-func newPushCommand() *cobra.Command {
+func NewPushCommand() *cobra.Command {
var pushCommand = &cobra.Command{
Use: "push [flags] NAME[:TAG]",
Short: "Push an image or a repository to a registry. Optionally specify \"ipfs://\" or \"ipns://\" scheme to push image to IPFS.",
- Args: IsExactArgs(1),
+ Args: helpers.IsExactArgs(1),
RunE: pushAction,
ValidArgsFunction: pushShellComplete,
SilenceUsage: true,
@@ -40,7 +43,7 @@ func newPushCommand() *cobra.Command {
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
pushCommand.Flags().StringSlice("platform", []string{}, "Push content for a specific platform")
- pushCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ pushCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
pushCommand.Flags().Bool("all-platforms", false, "Push content for all platforms")
// #endregion
@@ -57,6 +60,11 @@ func newPushCommand() *cobra.Command {
pushCommand.Flags().String("notation-key-name", "", "Signing key name for a key previously added to notation's key list for --sign=notation")
// #endregion
+ // #region soci flags
+ pushCommand.Flags().Int64("soci-span-size", -1, "Span size that soci index uses to segment layer data. Default is 4 MiB.")
+ pushCommand.Flags().Int64("soci-min-layer-size", -1, "Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.")
+ // #endregion
+
pushCommand.Flags().BoolP("quiet", "q", false, "Suppress verbose output")
pushCommand.Flags().Bool(allowNonDistFlag, false, "Allow pushing images with non-distributable blobs")
@@ -65,7 +73,7 @@ func newPushCommand() *cobra.Command {
}
func processImagePushOptions(cmd *cobra.Command) (types.ImagePushOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImagePushOptions{}, err
}
@@ -101,9 +109,14 @@ func processImagePushOptions(cmd *cobra.Command) (types.ImagePushOptions, error)
if err != nil {
return types.ImagePushOptions{}, err
}
+ sociOptions, err := processSociOptions(cmd)
+ if err != nil {
+ return types.ImagePushOptions{}, err
+ }
return types.ImagePushOptions{
GOptions: globalOptions,
SignOptions: signOptions,
+ SociOptions: sociOptions,
Platforms: platform,
AllPlatforms: allPlatforms,
Estargz: estargz,
@@ -133,5 +146,28 @@ func pushAction(cmd *cobra.Command, args []string) error {
func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
+}
+
+func processImageSignOptions(cmd *cobra.Command) (opt types.ImageSignOptions, err error) {
+ if opt.Provider, err = cmd.Flags().GetString("sign"); err != nil {
+ return
+ }
+ if opt.CosignKey, err = cmd.Flags().GetString("cosign-key"); err != nil {
+ return
+ }
+ if opt.NotationKeyName, err = cmd.Flags().GetString("notation-key-name"); err != nil {
+ return
+ }
+ return
+}
+
+func processSociOptions(cmd *cobra.Command) (opt types.SociOptions, err error) {
+ if opt.SpanSize, err = cmd.Flags().GetInt64("soci-span-size"); err != nil {
+ return
+ }
+ if opt.MinLayerSize, err = cmd.Flags().GetInt64("soci-min-layer-size"); err != nil {
+ return
+ }
+ return
}
diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go
new file mode 100644
index 00000000000..791141876ee
--- /dev/null
+++ b/cmd/nerdctl/image/image_push_linux_test.go
@@ -0,0 +1,271 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
+)
+
+func TestPush(t *testing.T) {
+ nerdtest.Setup()
+
+ var registryNoAuthHTTPRandom, registryNoAuthHTTPDefault, registryTokenAuthHTTPSRandom *testregistry.RegistryServer
+
+ testCase := &test.Case{
+ Require: test.Linux,
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ base := testutil.NewBase(t)
+ registryNoAuthHTTPRandom = testregistry.NewWithNoAuth(base, 0, false)
+ registryNoAuthHTTPDefault = testregistry.NewWithNoAuth(base, 80, false)
+ registryTokenAuthHTTPSRandom = testregistry.NewWithTokenAuth(base, "admin", "badmin", 0, true)
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if registryNoAuthHTTPRandom != nil {
+ registryNoAuthHTTPRandom.Cleanup(nil)
+ }
+ if registryNoAuthHTTPDefault != nil {
+ registryNoAuthHTTPDefault.Cleanup(nil)
+ }
+ if registryTokenAuthHTTPSRandom != nil {
+ registryTokenAuthHTTPSRandom.Cleanup(nil)
+ }
+ },
+
+ SubTests: []*test.Case{
+ {
+ Description: "plain http",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(1, []error{errors.New("server gave HTTP response to HTTPS client")}, nil),
+ },
+ {
+ Description: "plain http with insecure",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--insecure-registry", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "plain http with localhost",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ "127.0.0.1", registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "plain http with insecure, default port",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s/%s:%s",
+ registryNoAuthHTTPDefault.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--insecure-registry", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with insecure, with login",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ helpers.Ensure("--insecure-registry", "login", "-u", "admin", "-p", "badmin",
+ fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port))
+
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--insecure-registry", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with hosts dir, with login",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port, data.Identifier(), strings.Split(testutil.CommonImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.CommonImage, testImageRef)
+ helpers.Ensure("--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, "login", "-u", "admin", "-p", "badmin",
+ fmt.Sprintf("%s:%d", registryTokenAuthHTTPSRandom.IP.String(), registryTokenAuthHTTPSRandom.Port))
+
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "non distributable artifacts",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--insecure-registry", data.Get("testImageRef"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest)
+ resp, err := http.Get(blobURL)
+ assert.Assert(t, err, "error making http request")
+ if resp.Body != nil {
+ resp.Body.Close()
+ }
+ assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available")
+ },
+ }
+ },
+ },
+ {
+ Description: "non distributable artifacts (with)",
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.NonDistBlobImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.NonDistBlobImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.NonDistBlobImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Get("testImageRef"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), testutil.NonDistBlobDigest)
+ resp, err := http.Get(blobURL)
+ assert.Assert(t, err, "error making http request")
+ if resp.Body != nil {
+ resp.Body.Close()
+ }
+ assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available")
+ },
+ }
+ },
+ },
+ {
+ Description: "soci",
+ Require: test.Require(
+ nerdtest.Soci,
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.UbuntuImage)
+ testImageRef := fmt.Sprintf("%s:%d/%s:%s",
+ registryNoAuthHTTPRandom.IP.String(), registryNoAuthHTTPRandom.Port, data.Identifier(), strings.Split(testutil.UbuntuImage, ":")[1])
+ data.Set("testImageRef", testImageRef)
+ helpers.Ensure("tag", testutil.UbuntuImage, testImageRef)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("testImageRef") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("testImageRef"))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Get("testImageRef"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ },
+ }
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_remove.go b/cmd/nerdctl/image/image_remove.go
similarity index 84%
rename from cmd/nerdctl/image_remove.go
rename to cmd/nerdctl/image/image_remove.go
index b060ec27a9e..cb9a86c39d5 100644
--- a/cmd/nerdctl/image_remove.go
+++ b/cmd/nerdctl/image/image_remove.go
@@ -14,16 +14,19 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
-func newRmiCommand() *cobra.Command {
+func NewRmiCommand() *cobra.Command {
var rmiCommand = &cobra.Command{
Use: "rmi [flags] IMAGE [IMAGE, ...]",
Short: "Remove one or more images",
@@ -40,7 +43,7 @@ func newRmiCommand() *cobra.Command {
}
func processImageRemoveOptions(cmd *cobra.Command) (types.ImageRemoveOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageRemoveOptions{}, err
}
@@ -79,5 +82,5 @@ func rmiAction(cmd *cobra.Command, args []string) error {
func rmiShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go
new file mode 100644
index 00000000000..e7e0e4d2c92
--- /dev/null
+++ b/cmd/nerdctl/image/image_remove_test.go
@@ -0,0 +1,523 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "errors"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestRemove(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const (
+ imgShortIDKey = "imgShortID"
+ )
+
+ repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage)
+ nginxRepoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage)
+ // NOTES:
+ // - since all of these are rmi-ing the common image, we need private mode
+ testCase.Require = nerdtest.Private
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Remove image with stopped container - without -f",
+ NoParallel: true,
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("image is being used")},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with stopped container - with -f",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", "-f", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.DoesNotContain(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with running container - without -f",
+ NoParallel: true,
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("image is being used")},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with running container - with -f",
+ NoParallel: true,
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+
+ img := nerdtest.InspectImage(helpers, testutil.CommonImage)
+ repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage)
+ imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8]
+
+ data.Set(imgShortIDKey, imgShortID)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("rmi", "-f", data.Get(imgShortIDKey))
+ },
+ Command: test.Command("rmi", "-f", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(""),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with created container - without -f",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("image is being used")},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with created container - with -f",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage)
+ helpers.Ensure("create", "--pull", "always", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("rmi", testutil.NginxAlpineImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", "-f", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.All(
+ test.DoesNotContain(repoName),
+ // a created container with removed image doesn't impact other `rmi` command
+ test.DoesNotContain(nginxRepoName),
+ ),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with paused container - without -f",
+ NoParallel: true,
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ nerdtest.CGroup,
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("pause", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("image is being used")},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with paused container - with -f",
+ NoParallel: true,
+ Require: test.Require(
+ nerdtest.CGroup,
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("pause", data.Identifier())
+
+ img := nerdtest.InspectImage(helpers, testutil.CommonImage)
+ repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage)
+ imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8]
+
+ data.Set(imgShortIDKey, imgShortID)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("rmi", "-f", data.Get(imgShortIDKey))
+ },
+ Command: test.Command("rmi", "-f", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(""),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with killed container - without -f",
+ NoParallel: true,
+ Require: test.Require(
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("kill", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("image is being used")},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.Contains(repoName),
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Remove image with killed container - with -f",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "--pull", "always", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("kill", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("rmi", "-f", testutil.CommonImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images").Run(&test.Expected{
+ Output: test.DoesNotContain(repoName),
+ })
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+// TestIssue3016 tests https://github.com/containerd/nerdctl/issues/3016
+func TestIssue3016(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const (
+ tagIDKey = "tagID"
+ )
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Issue #3016 - Tags created using the short digest ids of container images cannot be deleted using the nerdctl rmi command.",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.CommonImage)
+ helpers.Ensure("pull", testutil.NginxAlpineImage)
+
+ img := nerdtest.InspectImage(helpers, testutil.NginxAlpineImage)
+ repoName, _ := imgutil.ParseRepoTag(testutil.NginxAlpineImage)
+ tagID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8]
+
+ helpers.Ensure("tag", testutil.CommonImage, tagID)
+
+ data.Set(tagIDKey, tagID)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("rmi", data.Get(tagIDKey))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("images", data.Get(tagIDKey)).Run(&test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Equal(t, len(strings.Split(stdout, "\n")), 2)
+ },
+ })
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestRemoveKubeWithKubeHideDupe(t *testing.T) {
+ var numTags, numNoTags int
+ testCase := nerdtest.Setup()
+ testCase.NoParallel = true
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage)
+ }
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ numTags = len(strings.Split(strings.TrimSpace(helpers.Capture("--kube-hide-dupe", "images")), "\n"))
+ numNoTags = len(strings.Split(strings.TrimSpace(helpers.Capture("images")), "\n"))
+ }
+ testCase.Require = test.Require(
+ nerdtest.OnlyKubernetes,
+ )
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "After removing the tag without kube-hide-dupe, repodigest is shown as ",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.BusyboxImage)
+ },
+ Command: test.Command("rmi", "-f", testutil.BusyboxImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numTags+1, info)
+ },
+ })
+ helpers.Command("images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numNoTags+1, info)
+ },
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "If there are other tags, the Repodigest will not be deleted",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("--kube-hide-dupe", "rmi", data.Identifier())
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.BusyboxImage)
+ helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
+ },
+ Command: test.Command("--kube-hide-dupe", "rmi", testutil.BusyboxImage),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numTags+1, info)
+ },
+ })
+ helpers.Command("images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numNoTags+2, info)
+ },
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "After deleting all repo:tag entries, all repodigests will be cleaned up",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.BusyboxImage)
+ helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ helpers.Ensure("--kube-hide-dupe", "rmi", "-f", testutil.BusyboxImage)
+ return helpers.Command("--kube-hide-dupe", "rmi", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("--kube-hide-dupe", "images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numTags, info)
+ },
+ })
+ helpers.Command("images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numNoTags, info)
+ },
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Test multiple IDs found with provided prefix and force with shortID",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.BusyboxImage)
+ helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Command("--kube-hide-dupe", "rmi", stdout[0:12]).Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("multiple IDs found with provided prefix: ")},
+ })
+ helpers.Command("--kube-hide-dupe", "rmi", "--force", stdout[0:12]).Run(&test.Expected{
+ ExitCode: 0,
+ })
+ helpers.Command("images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numNoTags, info)
+ },
+ })
+ },
+ }
+ },
+ },
+ {
+ Description: "Test remove image with digestID",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", testutil.BusyboxImage)
+ helpers.Ensure("tag", testutil.BusyboxImage, data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("--kube-hide-dupe", "images", testutil.BusyboxImage, "-q", "--no-trunc")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ imgID := strings.Split(stdout, "\n")
+ helpers.Command("--kube-hide-dupe", "rmi", imgID[0]).Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("multiple IDs found with provided prefix: ")},
+ })
+ helpers.Command("--kube-hide-dupe", "rmi", "--force", imgID[0]).Run(&test.Expected{
+ ExitCode: 0,
+ })
+ helpers.Command("images").Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) == numNoTags, info)
+ },
+ })
+ },
+ }
+ },
+ },
+ }
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_save.go b/cmd/nerdctl/image/image_save.go
similarity index 86%
rename from cmd/nerdctl/image_save.go
rename to cmd/nerdctl/image/image_save.go
index 3d9e9ca91cd..8c2ce38d309 100644
--- a/cmd/nerdctl/image_save.go
+++ b/cmd/nerdctl/image/image_save.go
@@ -14,20 +14,23 @@
limitations under the License.
*/
-package main
+package image
import (
"fmt"
"os"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
-func newSaveCommand() *cobra.Command {
+func NewSaveCommand() *cobra.Command {
var saveCommand = &cobra.Command{
Use: "save",
Args: cobra.MinimumNArgs(1),
@@ -43,7 +46,7 @@ func newSaveCommand() *cobra.Command {
// #region platform flags
// platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64"
saveCommand.Flags().StringSlice("platform", []string{}, "Export content for a specific platform")
- saveCommand.RegisterFlagCompletionFunc("platform", shellCompletePlatforms)
+ saveCommand.RegisterFlagCompletionFunc("platform", completion.Platforms)
saveCommand.Flags().Bool("all-platforms", false, "Export content for all platforms")
// #endregion
@@ -51,7 +54,7 @@ func newSaveCommand() *cobra.Command {
}
func processImageSaveOptions(cmd *cobra.Command) (types.ImageSaveOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.ImageSaveOptions{}, err
}
@@ -108,5 +111,5 @@ func saveAction(cmd *cobra.Command, args []string) error {
func saveShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go
new file mode 100644
index 00000000000..9e2dae5cda0
--- /dev/null
+++ b/cmd/nerdctl/image/image_save_test.go
@@ -0,0 +1,197 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestSaveContent(t *testing.T) {
+ nerdtest.Setup()
+
+ testCase := &test.Case{
+ // FIXME: move to busybox for windows?
+ Require: test.Not(test.Windows),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("save", "-o", filepath.Join(data.TempDir(), "out.tar"), testutil.CommonImage)
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ rootfsPath := filepath.Join(data.TempDir(), "rootfs")
+ err := testhelpers.ExtractDockerArchive(filepath.Join(data.TempDir(), "out.tar"), rootfsPath)
+ assert.NilError(t, err)
+ etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release")
+ etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath)
+ assert.NilError(t, err)
+ etcOSRelease := string(etcOSReleaseBytes)
+ assert.Assert(t, strings.Contains(etcOSRelease, "Alpine"))
+ },
+ }
+ },
+ }
+
+ testCase.Run(t)
+}
+
+func TestSave(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // This test relies on the fact that we can remove the common image, which definitely conflicts with others,
+ // hence the private mode.
+ // Further note though, that this will hide the fact this the save command could fail if some layers are missing.
+ // See https://github.com/containerd/nerdctl/issues/3425 and others for details.
+ testCase.Require = nerdtest.Private
+
+ if runtime.GOOS == "windows" {
+ testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524")
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Single image, by id",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("id") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("id"))
+ }
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ img := nerdtest.InspectImage(helpers, testutil.CommonImage)
+ var id string
+ // Docker and Nerdctl do not agree on what is the definition of an image ID
+ if nerdtest.IsDocker() {
+ id = img.ID
+ } else {
+ id = strings.Split(img.RepoDigests[0], ":")[1]
+ }
+ tarPath := filepath.Join(data.TempDir(), "out.tar")
+ helpers.Ensure("save", "-o", tarPath, id)
+ helpers.Ensure("rmi", "-f", testutil.CommonImage)
+ helpers.Ensure("load", "-i", tarPath)
+ data.Set("id", id)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo")
+ },
+ Expected: test.Expects(0, nil, test.Equals("foo\n")),
+ },
+ {
+ Description: "Image with different names, by id",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("id") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("id"))
+ }
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ img := nerdtest.InspectImage(helpers, testutil.CommonImage)
+ var id string
+ if nerdtest.IsDocker() {
+ id = img.ID
+ } else {
+ id = strings.Split(img.RepoDigests[0], ":")[1]
+ }
+ helpers.Ensure("tag", testutil.CommonImage, data.Identifier())
+ tarPath := filepath.Join(data.TempDir(), "out.tar")
+ helpers.Ensure("save", "-o", tarPath, id)
+ helpers.Ensure("rmi", "-f", testutil.CommonImage)
+ helpers.Ensure("load", "-i", tarPath)
+ data.Set("id", id)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get("id"), "sh", "-euxc", "echo foo")
+ },
+ Expected: test.Expects(0, nil, test.Equals("foo\n")),
+ },
+ }
+
+ testCase.Run(t)
+}
+
+// TestSaveMultipleImagesWithSameIDAndLoad tests https://github.com/containerd/nerdctl/issues/3806
+func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // This test relies on the fact that we can remove the common image, which definitely conflicts with others,
+ // hence the private mode.
+ // Further note though, that this will hide the fact this the save command could fail if some layers are missing.
+ // See https://github.com/containerd/nerdctl/issues/3425 and others for details.
+ testCase.Require = nerdtest.Private
+
+ if runtime.GOOS == "windows" {
+ testCase.Require = nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3524")
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Issue #3568 - Save multiple container images with the same image ID but different image names",
+ NoParallel: true,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("id") != "" {
+ helpers.Anyhow("rmi", "-f", data.Get("id"))
+ }
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ img := nerdtest.InspectImage(helpers, testutil.CommonImage)
+ var id string
+ if nerdtest.IsDocker() {
+ id = img.ID
+ } else {
+ id = strings.Split(img.RepoDigests[0], ":")[1]
+ }
+ helpers.Ensure("tag", testutil.CommonImage, data.Identifier())
+ tarPath := filepath.Join(data.TempDir(), "out.tar")
+ helpers.Ensure("save", "-o", tarPath, testutil.CommonImage, data.Identifier())
+ helpers.Ensure("rmi", "-f", id)
+ helpers.Ensure("load", "-i", tarPath)
+ data.Set("id", id)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("images", "--no-trunc")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: []error{},
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Equal(t, strings.Count(stdout, data.Get("id")), 2)
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/image_tag.go b/cmd/nerdctl/image/image_tag.go
similarity index 77%
rename from cmd/nerdctl/image_tag.go
rename to cmd/nerdctl/image/image_tag.go
index 4361eeffe3b..4e3d5b965fc 100644
--- a/cmd/nerdctl/image_tag.go
+++ b/cmd/nerdctl/image/image_tag.go
@@ -14,21 +14,23 @@
limitations under the License.
*/
-package main
+package image
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
)
-func newTagCommand() *cobra.Command {
+func NewTagCommand() *cobra.Command {
var tagCommand = &cobra.Command{
Use: "tag [flags] SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]",
Short: "Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE",
- Args: IsExactArgs(2),
+ Args: helpers.IsExactArgs(2),
RunE: tagAction,
ValidArgsFunction: tagShellComplete,
SilenceUsage: true,
@@ -38,7 +40,7 @@ func newTagCommand() *cobra.Command {
}
func tagAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -61,7 +63,7 @@ func tagAction(cmd *cobra.Command, args []string) error {
func tagShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) < 2 {
// show image names
- return shellCompleteImageNames(cmd)
+ return completion.ImageNames(cmd)
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
diff --git a/cmd/nerdctl/image/image_test.go b/cmd/nerdctl/image/image_test.go
new file mode 100644
index 00000000000..4cbba7d968f
--- /dev/null
+++ b/cmd/nerdctl/image/image_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/image_convert_linux_test.go b/cmd/nerdctl/image_convert_linux_test.go
deleted file mode 100644
index 4b7eff95856..00000000000
--- a/cmd/nerdctl/image_convert_linux_test.go
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "runtime"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "gotest.tools/v3/icmd"
-)
-
-func TestImageConvertNydus(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("no windows support yet")
- }
- testutil.RequireExecutable(t, "nydus-image")
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- convertedImage := testutil.Identifier(t) + ":nydus"
- base.Cmd("rmi", convertedImage).Run()
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("image", "convert", "--nydus", "--oci",
- testutil.CommonImage, convertedImage).AssertOK()
- defer base.Cmd("rmi", convertedImage).Run()
-
- // use `nydusify` check whether the convertd nydus image is valid
-
- // skip if rootless
- if rootlessutil.IsRootless() {
- t.Skip("Nydusify check is not supported rootless mode.")
- }
-
- // skip if nydusify is not installed
- testutil.RequireExecutable(t, "nydusify")
-
- // setup local docker registry
- registryPort := 15000
- registry := testregistry.NewPlainHTTP(base, registryPort)
- defer registry.Cleanup()
-
- remoteImage := fmt.Sprintf("%s:%d/nydusd-image:test", registry.IP.String(), registryPort)
- base.Cmd("tag", convertedImage, remoteImage).AssertOK()
- defer base.Cmd("rmi", remoteImage).Run()
- base.Cmd("push", "--insecure-registry", remoteImage).AssertOK()
- nydusifyCmd := testutil.Cmd{
- Cmd: icmd.Command(
- "nydusify",
- "check",
- "--source",
- testutil.CommonImage,
- "--target",
- remoteImage,
- "--source-insecure",
- "--target-insecure",
- ),
- Base: base,
- }
- nydusifyCmd.AssertOK()
-}
diff --git a/cmd/nerdctl/image_convert_test.go b/cmd/nerdctl/image_convert_test.go
deleted file mode 100644
index 4853c1070ce..00000000000
--- a/cmd/nerdctl/image_convert_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "runtime"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestImageConvertEStargz(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("no windows support yet")
- }
- testutil.DockerIncompatible(t)
- t.Parallel()
- base := testutil.NewBase(t)
- convertedImage := testutil.Identifier(t) + ":esgz"
- base.Cmd("rmi", convertedImage).Run()
- defer base.Cmd("rmi", convertedImage).Run()
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("image", "convert", "--estargz", "--oci",
- testutil.CommonImage, convertedImage).AssertOK()
-}
-
-func TestImageConvertZstdChunked(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("no windows support yet")
- }
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- convertedImage := testutil.Identifier(t) + ":zstdchunked"
- base.Cmd("rmi", convertedImage).Run()
- defer base.Cmd("rmi", convertedImage).Run()
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("image", "convert", "--zstdchunked", "--oci", "--zstdchunked-compression-level", "3",
- testutil.CommonImage, convertedImage).AssertOK()
-}
diff --git a/cmd/nerdctl/image_encrypt_linux_test.go b/cmd/nerdctl/image_encrypt_linux_test.go
deleted file mode 100644
index 9236a5ca33a..00000000000
--- a/cmd/nerdctl/image_encrypt_linux_test.go
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "context"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "testing"
-
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "gotest.tools/v3/assert"
-)
-
-type jweKeyPair struct {
- prv string
- pub string
- cleanup func()
-}
-
-func newJWEKeyPair(t testing.TB) *jweKeyPair {
- testutil.RequireExecutable(t, "openssl")
- td, err := os.MkdirTemp(t.TempDir(), "jwe-key-pair")
- assert.NilError(t, err)
- prv := filepath.Join(td, "mykey.pem")
- pub := filepath.Join(td, "mypubkey.pem")
- cmds := [][]string{
- // Exec openssl commands to ensure that nerdctl is compatible with the output of openssl commands.
- // Do NOT refactor this function to use "crypto/rsa" stdlib.
- {"openssl", "genrsa", "-out", prv},
- {"openssl", "rsa", "-in", prv, "-pubout", "-out", pub},
- }
- for _, f := range cmds {
- cmd := exec.Command(f[0], f[1:]...)
- if out, err := cmd.CombinedOutput(); err != nil {
- t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out))
- }
- }
- return &jweKeyPair{
- prv: prv,
- pub: pub,
- cleanup: func() {
- _ = os.RemoveAll(td)
- },
- }
-}
-
-func rmiAll(base *testutil.Base) {
- base.T.Logf("Pruning images")
- imageIDs := base.Cmd("images", "--no-trunc", "-a", "-q").OutLines()
- // remove empty output line at the end
- imageIDs = imageIDs[:len(imageIDs)-1]
- // use `Run` on purpose (same below) because `rmi all` may fail on individual
- // image id that has an expected running container (e.g. a registry)
- base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run()
-
- base.T.Logf("Pruning build caches")
- if _, err := buildkitutil.GetBuildkitHost(testutil.Namespace); err == nil {
- base.Cmd("builder", "prune").AssertOK()
- }
-
- // For BuildKit >= 0.11, pruning cache isn't enough to remove manifest blobs that are referred by build history blobs
- // https://github.com/containerd/nerdctl/pull/1833
- if base.Target == testutil.Nerdctl {
- base.T.Logf("Pruning all content blobs")
- addr := base.ContainerdAddress()
- client, err := containerd.New(addr, containerd.WithDefaultNamespace(testutil.Namespace))
- assert.NilError(base.T, err)
- cs := client.ContentStore()
- ctx := context.TODO()
- wf := func(info content.Info) error {
- base.T.Logf("Pruning blob %+v", info)
- if err := cs.Delete(ctx, info.Digest); err != nil {
- base.T.Log(err)
- }
- return nil
- }
- if err := cs.Walk(ctx, wf); err != nil {
- base.T.Log(err)
- }
-
- base.T.Logf("Pruning all images (again?)")
- imageIDs = base.Cmd("images", "--no-trunc", "-a", "-q").OutLines()
- base.T.Logf("pruning following images: %+v", imageIDs)
- base.Cmd(append([]string{"rmi", "-f"}, imageIDs...)...).Run()
- }
-}
-
-func TestImageEncryptJWE(t *testing.T) {
- testutil.DockerIncompatible(t)
- keyPair := newJWEKeyPair(t)
- defer keyPair.cleanup()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- encryptImageRef := fmt.Sprintf("127.0.0.1:%d/%s:encrypted", reg.ListenPort, tID)
- defer base.Cmd("rmi", encryptImageRef).Run()
- base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.pub, testutil.CommonImage, encryptImageRef).AssertOK()
- base.Cmd("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", encryptImageRef).AssertOutExactly("1\n")
- base.Cmd("image", "inspect", "--mode=native", "--format={{json .Manifest.Layers}}", encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe")
- base.Cmd("push", encryptImageRef).AssertOK()
- // remove all local images (in the nerdctl-test namespace), to ensure that we do not have blobs of the original image.
- rmiAll(base)
- base.Cmd("pull", encryptImageRef).AssertFail() // defaults to --unpack=true, and fails due to missing prv key
- base.Cmd("pull", "--unpack=false", encryptImageRef).AssertOK()
- decryptImageRef := tID + ":decrypted"
- defer base.Cmd("rmi", decryptImageRef).Run()
- base.Cmd("image", "decrypt", "--key="+keyPair.pub, encryptImageRef, decryptImageRef).AssertFail() // decryption needs prv key, not pub key
- base.Cmd("image", "decrypt", "--key="+keyPair.prv, encryptImageRef, decryptImageRef).AssertOK()
-}
diff --git a/cmd/nerdctl/image_inspect_test.go b/cmd/nerdctl/image_inspect_test.go
deleted file mode 100644
index ef35921ef03..00000000000
--- a/cmd/nerdctl/image_inspect_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestImageInspectContainsSomeStuff(t *testing.T) {
- base := testutil.NewBase(t)
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- inspect := base.InspectImage(testutil.CommonImage)
-
- assert.Assert(base.T, len(inspect.RootFS.Layers) > 0)
- assert.Assert(base.T, inspect.RootFS.Type != "")
- assert.Assert(base.T, inspect.Architecture != "")
- assert.Assert(base.T, inspect.Size > 0)
-}
-
-func TestImageInspectWithFormat(t *testing.T) {
- base := testutil.NewBase(t)
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- // test RawFormat support
- base.Cmd("image", "inspect", testutil.CommonImage, "--format", "{{.Id}}").AssertOK()
-
- // test typedFormat support
- base.Cmd("image", "inspect", testutil.CommonImage, "--format", "{{.ID}}").AssertOK()
-}
diff --git a/cmd/nerdctl/image_list_test.go b/cmd/nerdctl/image_list_test.go
deleted file mode 100644
index 1b07841fcec..00000000000
--- a/cmd/nerdctl/image_list_test.go
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/tabutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestImagesWithNames(t *testing.T) {
- t.Parallel()
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("images", "--names", testutil.CommonImage).AssertOutContains(testutil.CommonImage)
- base.Cmd("images", "--names", testutil.CommonImage).AssertOutWithFunc(func(out string) error {
- lines := strings.Split(strings.TrimSpace(out), "\n")
- if len(lines) < 2 {
- return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
- }
- tab := tabutil.NewReader("NAME\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE")
- err := tab.ParseHeader(lines[0])
- if err != nil {
- return fmt.Errorf("failed to parse header: %v", err)
- }
- name, _ := tab.ReadRow(lines[1], "NAME")
- assert.Equal(t, name, testutil.CommonImage)
- return nil
- })
-}
-
-func TestImages(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- header := "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tPLATFORM\tSIZE\tBLOB SIZE"
- if base.Target == testutil.Docker {
- header = "REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE"
- }
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("images", testutil.CommonImage).AssertOutWithFunc(func(out string) error {
- lines := strings.Split(strings.TrimSpace(out), "\n")
- if len(lines) < 2 {
- return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
- }
- tab := tabutil.NewReader(header)
- err := tab.ParseHeader(lines[0])
- if err != nil {
- return fmt.Errorf("failed to parse header: %v", err)
- }
- repo, _ := tab.ReadRow(lines[1], "REPOSITORY")
- tag, _ := tab.ReadRow(lines[1], "TAG")
- assert.Equal(t, repo+":"+tag, testutil.CommonImage)
- return nil
- })
-}
-
-func TestImagesFilter(t *testing.T) {
- testutil.RequiresBuild(t)
- t.Parallel()
- base := testutil.NewBase(t)
- tempName := testutil.Identifier(base.T)
- base.Cmd("pull", testutil.CommonImage).AssertOK()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"] \n
-LABEL foo=bar
-LABEL version=0.1`, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
- base.Cmd("build", "-t", tempName, "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK()
- defer base.Cmd("rmi", tempName).AssertOK()
-
- busyboxGlibc, busyboxUclibc := "busybox:glibc", "busybox:uclibc"
- base.Cmd("pull", busyboxGlibc).AssertOK()
- defer base.Cmd("rmi", busyboxGlibc).AssertOK()
-
- base.Cmd("pull", busyboxUclibc).AssertOK()
- defer base.Cmd("rmi", busyboxUclibc).AssertOK()
-
- base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutContains(testutil.ImageRepo(testutil.CommonImage))
- base.Cmd("images", "--filter", fmt.Sprintf("before=%s:%s", tempName, "latest")).AssertOutNotContains(tempName)
- base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutContains(tempName)
- base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage)).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage))
- base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(testutil.ImageRepo(testutil.CommonImage))
- base.Cmd("images", "--filter", fmt.Sprintf("since=%s", testutil.CommonImage), testutil.CommonImage).AssertOutNotContains(tempName)
- base.Cmd("images", "--filter", "label=foo=bar").AssertOutContains(tempName)
- base.Cmd("images", "--filter", "label=foo=bar1").AssertOutNotContains(tempName)
- base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.1").AssertOutContains(tempName)
- base.Cmd("images", "--filter", "label=foo=bar", "--filter", "label=version=0.2").AssertOutNotContains(tempName)
- base.Cmd("images", "--filter", "label=version").AssertOutContains(tempName)
- base.Cmd("images", "--filter", fmt.Sprintf("reference=%s*", tempName)).AssertOutContains(tempName)
- base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("glibc")
- base.Cmd("images", "--filter", "reference=busy*:*libc*").AssertOutContains("uclibc")
-}
-
-func TestImagesFilterDangling(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- base.Cmd("images", "prune", "--all").AssertOK()
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-notag-string"]
- `, testutil.CommonImage)
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
-
- defer os.RemoveAll(buildCtx)
- base.Cmd("build", "-f", buildCtx+"/Dockerfile", buildCtx).AssertOK()
-
- // dangling image test
- base.Cmd("images", "--filter", "dangling=true").AssertOutContains("")
- base.Cmd("images", "--filter", "dangling=false").AssertOutNotContains("")
-}
diff --git a/cmd/nerdctl/image_load_linux_test.go b/cmd/nerdctl/image_load_linux_test.go
deleted file mode 100644
index 0f620363a19..00000000000
--- a/cmd/nerdctl/image_load_linux_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestLoadStdinFromPipe(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
-
- tmp := t.TempDir()
- img := testutil.Identifier(t) + "image"
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- base.Cmd("tag", testutil.CommonImage, img).AssertOK()
- base.Cmd("save", img, "-o", filepath.Join(tmp, "common.tar")).AssertOK()
- base.Cmd("rmi", "-f", img).AssertOK()
- loadCmd := strings.Join(base.Cmd("load").Command, " ")
- output := filepath.Join(tmp, "output")
-
- combined, err := exec.Command("sh", "-euxc", fmt.Sprintf("`cat %s/common.tar | %s > %s`", tmp, loadCmd, output)).CombinedOutput()
- assert.NilError(t, err, "failed with error %s and combined output is %s", err, string(combined))
- fb, err := os.ReadFile(output)
- assert.NilError(t, err)
-
- assert.Assert(t, strings.Contains(string(fb), fmt.Sprintf("Loaded image: %s:latest", img)))
- base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage))
-}
-
-func TestLoadStdinEmpty(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- base.Cmd("load").AssertFail()
-}
diff --git a/cmd/nerdctl/image_prune_test.go b/cmd/nerdctl/image_prune_test.go
deleted file mode 100644
index a185d6f33c2..00000000000
--- a/cmd/nerdctl/image_prune_test.go
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestImagePrune(t *testing.T) {
- testutil.RequiresBuild(t)
-
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").AssertOK()
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).AssertOK()
-
- dockerfile := fmt.Sprintf(`FROM %s
- CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", buildCtx).AssertOK()
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("images").AssertOutContainsAll(imageName, "")
-
- base.Cmd("image", "prune", "--force").AssertNoOut(imageName)
- base.Cmd("images").AssertNoOut("")
- base.Cmd("images").AssertOutContains(imageName)
-}
-
-func TestImagePruneAll(t *testing.T) {
- testutil.RequiresBuild(t)
-
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").AssertOK()
- imageName := testutil.Identifier(t)
-
- dockerfile := fmt.Sprintf(`FROM %s
- CMD ["echo", "nerdctl-test-image-prune"]`, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- // The following commands will clean up all images, so it should fail at this point.
- defer base.Cmd("rmi", imageName).AssertFail()
- base.Cmd("images").AssertOutContains(imageName)
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "--name", tID, imageName).AssertOK()
- base.Cmd("image", "prune", "--force", "--all").AssertNoOut(imageName)
- base.Cmd("images").AssertOutContains(imageName)
-
- base.Cmd("rm", "-f", tID).AssertOK()
- base.Cmd("image", "prune", "--force", "--all").AssertOutContains(imageName)
- base.Cmd("images").AssertNoOut(imageName)
-}
diff --git a/cmd/nerdctl/image_pull_linux_test.go b/cmd/nerdctl/image_pull_linux_test.go
deleted file mode 100644
index 777e00120f8..00000000000
--- a/cmd/nerdctl/image_pull_linux_test.go
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "gotest.tools/v3/assert"
-)
-
-type cosignKeyPair struct {
- publicKey string
- privateKey string
- cleanup func()
-}
-
-func newCosignKeyPair(t testing.TB, path string) *cosignKeyPair {
- td, err := os.MkdirTemp(t.TempDir(), path)
- assert.NilError(t, err)
-
- cmd := exec.Command("cosign", "generate-key-pair")
- cmd.Dir = td
- if out, err := cmd.CombinedOutput(); err != nil {
- t.Fatalf("failed to run %v: %v (%q)", cmd.Args, err, string(out))
- }
-
- publicKey := filepath.Join(td, "cosign.pub")
- privateKey := filepath.Join(td, "cosign.key")
-
- return &cosignKeyPair{
- publicKey: publicKey,
- privateKey: privateKey,
- cleanup: func() {
- _ = os.RemoveAll(td)
- },
- }
-}
-
-func TestImageVerifyWithCosign(t *testing.T) {
- testutil.RequireExecutable(t, "cosign")
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- t.Setenv("COSIGN_PASSWORD", "1")
- keyPair := newCosignKeyPair(t, "cosign-key-pair")
- defer keyPair.cleanup()
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- localhostIP := "127.0.0.1"
- t.Logf("localhost IP=%q", localhostIP)
- testImageRef := fmt.Sprintf("%s:%d/%s",
- localhostIP, reg.ListenPort, tID)
- t.Logf("testImageRef=%q", testImageRef)
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
- base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK()
- base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK()
-}
-
-func TestImagePullPlainHttpWithDefaultPort(t *testing.T) {
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- reg := testregistry.NewPlainHTTP(base, 80)
- defer reg.Cleanup()
- testImageRef := fmt.Sprintf("%s/%s:%s",
- reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- t.Logf("testImageRef=%q", testImageRef)
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
- base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
- base.Cmd("--insecure-registry", "push", testImageRef).AssertOK()
- base.Cmd("--insecure-registry", "pull", testImageRef).AssertOK()
-}
-
-func TestImageVerifyWithCosignShouldFailWhenKeyIsNotCorrect(t *testing.T) {
- testutil.RequireExecutable(t, "cosign")
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- t.Setenv("COSIGN_PASSWORD", "1")
- keyPair := newCosignKeyPair(t, "cosign-key-pair")
- defer keyPair.cleanup()
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- tID := testutil.Identifier(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- localhostIP := "127.0.0.1"
- t.Logf("localhost IP=%q", localhostIP)
- testImageRef := fmt.Sprintf("%s:%d/%s",
- localhostIP, reg.ListenPort, tID)
- t.Logf("testImageRef=%q", testImageRef)
-
- dockerfile := fmt.Sprintf(`FROM %s
-CMD ["echo", "nerdctl-build-test-string"]
- `, testutil.CommonImage)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- base.Cmd("build", "-t", testImageRef, buildCtx).AssertOK()
- base.Cmd("push", testImageRef, "--sign=cosign", "--cosign-key="+keyPair.privateKey).AssertOK()
- base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+keyPair.publicKey).AssertOK()
-
- t.Setenv("COSIGN_PASSWORD", "2")
- newKeyPair := newCosignKeyPair(t, "cosign-key-pair-test")
- base.Cmd("pull", testImageRef, "--verify=cosign", "--cosign-key="+newKeyPair.publicKey).AssertFail()
-}
diff --git a/cmd/nerdctl/image_push_linux_test.go b/cmd/nerdctl/image_push_linux_test.go
deleted file mode 100644
index bfe8cb02bcf..00000000000
--- a/cmd/nerdctl/image_push_linux_test.go
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "net/http"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
- "gotest.tools/v3/assert"
-)
-
-func TestPushPlainHTTPFails(t *testing.T) {
- base := testutil.NewBase(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s:%d/%s:%s",
- reg.IP.String(), reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- res := base.Cmd("push", testImageRef).Run()
- resCombined := res.Combined()
- t.Logf("result: exitCode=%d, out=%q", res.ExitCode, res.Combined())
- assert.Assert(t, res.ExitCode != 0)
- assert.Assert(t, strings.Contains(resCombined, "server gave HTTP response to HTTPS client"))
-}
-
-func TestPushPlainHTTPLocalhost(t *testing.T) {
- base := testutil.NewBase(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
- localhostIP := "127.0.0.1"
- t.Logf("localhost IP=%q", localhostIP)
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s:%d/%s:%s",
- localhostIP, reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- base.Cmd("push", testImageRef).AssertOK()
-}
-
-func TestPushPlainHTTPInsecure(t *testing.T) {
- // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s:%d/%s:%s",
- reg.IP.String(), reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- base.Cmd("--insecure-registry", "push", testImageRef).AssertOK()
-}
-
-func TestPushPlainHttpInsecureWithDefaultPort(t *testing.T) {
- // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewPlainHTTP(base, 80)
- defer reg.Cleanup()
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s/%s:%s",
- reg.IP.String(), testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- base.Cmd("--insecure-registry", "push", testImageRef).AssertOK()
-}
-
-func TestPushInsecureWithLogin(t *testing.T) {
- // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewHTTPS(base, "admin", "badmin")
- defer reg.Cleanup()
-
- base.Cmd("--insecure-registry", "login", "-u", "admin", "-p", "badmin",
- fmt.Sprintf("%s:%d", reg.IP.String(), reg.ListenPort)).AssertOK()
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s:%d/%s:%s",
- reg.IP.String(), reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- base.Cmd("push", testImageRef).AssertFail()
- base.Cmd("--insecure-registry", "push", testImageRef).AssertOK()
-}
-
-func TestPushWithHostsDir(t *testing.T) {
- // Skip docker, because Docker doesn't have `--hosts-dir` option, and we don't want to contaminate the global /etc/docker/certs.d during this test
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewHTTPS(base, "admin", "badmin")
- defer reg.Cleanup()
-
- base.Cmd("--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "badmin", fmt.Sprintf("%s:%d", reg.IP.String(), reg.ListenPort)).AssertOK()
-
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- testImageRef := fmt.Sprintf("%s:%d/%s:%s",
- reg.IP.String(), reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.CommonImage, ":")[1])
- t.Logf("testImageRef=%q", testImageRef)
- base.Cmd("tag", testutil.CommonImage, testImageRef).AssertOK()
-
- base.Cmd("--debug", "--hosts-dir", reg.HostsDir, "push", testImageRef).AssertOK()
-}
-
-func TestPushNonDistributableArtifacts(t *testing.T) {
- // Skip docker, because "dockerd --insecure-registries" requires restarting the daemon
- // Skip docker, because "--allow-nondistributable-artifacts" is a daemon-only option and requires restarting the daemon
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewPlainHTTP(base, 5000)
- defer reg.Cleanup()
-
- base.Cmd("pull", testutil.NonDistBlobImage).AssertOK()
-
- testImgRef := fmt.Sprintf("%s:%d/%s:%s",
- reg.IP.String(), reg.ListenPort, testutil.Identifier(t), strings.Split(testutil.NonDistBlobImage, ":")[1])
- base.Cmd("tag", testutil.NonDistBlobImage, testImgRef).AssertOK()
-
- base.Cmd("--debug", "--insecure-registry", "push", testImgRef).AssertOK()
-
- blobURL := fmt.Sprintf("http://%s:%d/v2/%s/blobs/%s", reg.IP.String(), reg.ListenPort, testutil.Identifier(t), testutil.NonDistBlobDigest)
- resp, err := http.Get(blobURL)
- assert.Assert(t, err, "error making http request")
- if resp.Body != nil {
- resp.Body.Close()
- }
- assert.Equal(t, resp.StatusCode, http.StatusNotFound, "non-distributable blob should not be available")
-
- base.Cmd("--debug", "--insecure-registry", "push", "--allow-nondistributable-artifacts", testImgRef).AssertOK()
- resp, err = http.Get(blobURL)
- assert.Assert(t, err, "error making http request")
- if resp.Body != nil {
- resp.Body.Close()
- }
- assert.Equal(t, resp.StatusCode, http.StatusOK, "non-distributable blob should be available")
-}
diff --git a/cmd/nerdctl/image_remove_linux_test.go b/cmd/nerdctl/image_remove_linux_test.go
deleted file mode 100644
index 7594798f62d..00000000000
--- a/cmd/nerdctl/image_remove_linux_test.go
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestRemoveImage(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- base.Cmd("image", "prune", "--force", "--all").AssertOK()
-
- // ignore error
- base.Cmd("rmi", "-f", tID).AssertOK()
-
- base.Cmd("run", "--name", tID, testutil.CommonImage).AssertOK()
- defer base.Cmd("rm", "-f", tID).AssertOK()
-
- base.Cmd("rmi", testutil.CommonImage).AssertFail()
- defer base.Cmd("rmi", "-f", testutil.CommonImage).Run()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK()
-
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.CommonImage))
-}
-
-func TestRemoveRunningImage(t *testing.T) {
- // If an image is associated with a running/paused containers, `docker rmi -f imageName`
- // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails.
- // In both cases, `nerdctl rmi -f` will fail.
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID).AssertOK()
-
- base.Cmd("rmi", testutil.CommonImage).AssertFail()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail()
- base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage))
-
- base.Cmd("kill", tID).AssertOK()
- base.Cmd("rmi", testutil.CommonImage).AssertFail()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK()
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.CommonImage))
-}
-
-func TestRemovePausedImage(t *testing.T) {
- // If an image is associated with a running/paused containers, `docker rmi -f imageName`
- // untags `imageName` (left a `` image) without deletion; `docker rmi -rf imageID` fails.
- // In both cases, `nerdctl rmi -f` will fail.
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- switch base.Info().CgroupDriver {
- case "none", "":
- t.Skip("requires cgroup (for pausing)")
- }
- tID := testutil.Identifier(t)
-
- base.Cmd("run", "--name", tID, "-d", testutil.CommonImage, "sleep", "infinity").AssertOK()
- base.Cmd("pause", tID).AssertOK()
- defer base.Cmd("rm", "-f", tID).AssertOK()
-
- base.Cmd("rmi", testutil.CommonImage).AssertFail()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertFail()
- base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage))
-
- base.Cmd("kill", tID).AssertOK()
- base.Cmd("rmi", testutil.CommonImage).AssertFail()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK()
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.CommonImage))
-}
-
-func TestRemoveImageWithCreatedContainer(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- base.Cmd("pull", testutil.AlpineImage).AssertOK()
- base.Cmd("pull", testutil.NginxAlpineImage).AssertOK()
-
- base.Cmd("create", "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID).AssertOK()
-
- base.Cmd("rmi", testutil.AlpineImage).AssertFail()
- base.Cmd("rmi", "-f", testutil.AlpineImage).AssertOK()
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.AlpineImage))
-
- // a created container with removed image doesn't impact other `rmi` command
- base.Cmd("rmi", "-f", testutil.NginxAlpineImage).AssertOK()
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.NginxAlpineImage))
-}
diff --git a/cmd/nerdctl/image_save_linux_test.go b/cmd/nerdctl/image_save_linux_test.go
deleted file mode 100644
index c2970a9f04c..00000000000
--- a/cmd/nerdctl/image_save_linux_test.go
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-
- "gotest.tools/v3/assert"
-)
-
-func TestSave(t *testing.T) {
- base := testutil.NewBase(t)
- base.Cmd("pull", testutil.AlpineImage).AssertOK()
- archiveTarPath := filepath.Join(t.TempDir(), "a.tar")
- base.Cmd("save", "-o", archiveTarPath, testutil.AlpineImage).AssertOK()
- rootfsPath := filepath.Join(t.TempDir(), "rootfs")
- err := extractDockerArchive(archiveTarPath, rootfsPath)
- assert.NilError(t, err)
- etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release")
- etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath)
- assert.NilError(t, err)
- etcOSRelease := string(etcOSReleaseBytes)
- t.Logf("read %q, extracted from %q", etcOSReleasePath, testutil.AlpineImage)
- t.Log(etcOSRelease)
- assert.Assert(t, strings.Contains(etcOSRelease, "Alpine"))
-}
-
-func extractDockerArchive(archiveTarPath, rootfsPath string) error {
- if err := os.MkdirAll(rootfsPath, 0755); err != nil {
- return err
- }
- workDir, err := os.MkdirTemp("", "extract-docker-archive")
- if err != nil {
- return err
- }
- defer os.RemoveAll(workDir)
- if err := extractTarFile(workDir, archiveTarPath); err != nil {
- return err
- }
- manifestJSONPath := filepath.Join(workDir, "manifest.json")
- manifestJSONBytes, err := os.ReadFile(manifestJSONPath)
- if err != nil {
- return err
- }
- var mani DockerArchiveManifestJSON
- if err := json.Unmarshal(manifestJSONBytes, &mani); err != nil {
- return err
- }
- if len(mani) > 1 {
- return fmt.Errorf("multi-image archive cannot be extracted: contains %d images", len(mani))
- }
- if len(mani) < 1 {
- return errors.New("invalid archive")
- }
- ent := mani[0]
- for _, l := range ent.Layers {
- layerTarPath := filepath.Join(workDir, l)
- if err := extractTarFile(rootfsPath, layerTarPath); err != nil {
- return err
- }
- }
- return nil
-}
-
-type DockerArchiveManifestJSON []DockerArchiveManifestJSONEntry
-
-type DockerArchiveManifestJSONEntry struct {
- Config string
- RepoTags []string
- Layers []string
-}
-
-func extractTarFile(dirPath, tarFilePath string) error {
- cmd := exec.Command("tar", "Cxf", dirPath, tarFilePath)
- if out, err := cmd.CombinedOutput(); err != nil {
- return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
- }
- return nil
-}
diff --git a/cmd/nerdctl/image_save_test.go b/cmd/nerdctl/image_save_test.go
deleted file mode 100644
index cd4106b8bf7..00000000000
--- a/cmd/nerdctl/image_save_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "path/filepath"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestSaveById(t *testing.T) {
- base := testutil.NewBase(t)
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- inspect := base.InspectImage(testutil.CommonImage)
- var id string
- if testutil.GetTarget() == testutil.Docker {
- id = inspect.ID
- } else {
- id = strings.Split(inspect.RepoDigests[0], ":")[1]
- }
- archiveTarPath := filepath.Join(t.TempDir(), "id.tar")
- base.Cmd("save", "-o", archiveTarPath, id).AssertOK()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK()
- base.Cmd("load", "-i", archiveTarPath).AssertOK()
- base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK()
-}
-
-func TestSaveByIdWithDifferentNames(t *testing.T) {
- base := testutil.NewBase(t)
- base.Cmd("pull", testutil.CommonImage).AssertOK()
- inspect := base.InspectImage(testutil.CommonImage)
- var id string
- if testutil.GetTarget() == testutil.Docker {
- id = inspect.ID
- } else {
- id = strings.Split(inspect.RepoDigests[0], ":")[1]
- }
-
- base.Cmd("tag", testutil.CommonImage, "foobar").AssertOK()
-
- archiveTarPath := filepath.Join(t.TempDir(), "id.tar")
- base.Cmd("save", "-o", archiveTarPath, id).AssertOK()
- base.Cmd("rmi", "-f", testutil.CommonImage).AssertOK()
- base.Cmd("load", "-i", archiveTarPath).AssertOK()
- base.Cmd("run", "--rm", id, "sh", "-euxc", "echo foo").AssertOK()
-}
diff --git a/cmd/nerdctl/inspect.go b/cmd/nerdctl/inspect/inspect.go
similarity index 81%
rename from cmd/nerdctl/inspect.go
rename to cmd/nerdctl/inspect/inspect.go
index 30204ca7c5c..a91624b27f1 100644
--- a/cmd/nerdctl/inspect.go
+++ b/cmd/nerdctl/inspect/inspect.go
@@ -14,23 +14,27 @@
limitations under the License.
*/
-package main
+package inspect
import (
"context"
"fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/cmd/image"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ containerCmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/container"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ imageCmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/image"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
)
-func newInspectCommand() *cobra.Command {
+func NewInspectCommand() *cobra.Command {
var inspectCommand = &cobra.Command{
Use: "inspect",
Short: "Return low-level information on objects.",
@@ -52,6 +56,8 @@ var validInspectType = map[string]bool{
}
func addInspectFlags(cmd *cobra.Command) {
+ cmd.Flags().BoolP("size", "s", false, "Display total file sizes (for containers)")
+
cmd.Flags().StringP("format", "f", "", "Format the output using the given Go template, e.g, '{{json .}}'")
cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
@@ -67,7 +73,7 @@ func addInspectFlags(cmd *cobra.Command) {
}
func inspectAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -111,13 +117,13 @@ func inspectAction(cmd *cobra.Command, args []string) error {
var containerInspectOptions types.ContainerInspectOptions
if inspectImage {
platform := ""
- imageInspectOptions, err = processImageInspectOptions(cmd, &platform)
+ imageInspectOptions, err = imageCmd.ProcessImageInspectOptions(cmd, &platform)
if err != nil {
return err
}
}
if inspectContainer {
- containerInspectOptions, err = processContainerInspectOptions(cmd)
+ containerInspectOptions, err = containerCmd.ProcessContainerInspectOptions(cmd)
if err != nil {
return err
}
@@ -163,8 +169,8 @@ func inspectAction(cmd *cobra.Command, args []string) error {
func inspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show container names
- containers, _ := shellCompleteContainerNames(cmd, nil)
+ containers, _ := completion.ContainerNames(cmd, nil)
// show image names
- images, _ := shellCompleteImageNames(cmd)
+ images, _ := completion.ImageNames(cmd)
return append(containers, images...), cobra.ShellCompDirectiveNoFileComp
}
diff --git a/cmd/nerdctl/inspect/inspect_test.go b/cmd/nerdctl/inspect/inspect_test.go
new file mode 100644
index 00000000000..18b6d6ffee5
--- /dev/null
+++ b/cmd/nerdctl/inspect/inspect_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package inspect
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/internal.go b/cmd/nerdctl/internal/internal.go
similarity index 93%
rename from cmd/nerdctl/internal.go
rename to cmd/nerdctl/internal/internal.go
index 9f62d1ea2ae..c159452823a 100644
--- a/cmd/nerdctl/internal.go
+++ b/cmd/nerdctl/internal/internal.go
@@ -14,13 +14,13 @@
limitations under the License.
*/
-package main
+package internal
import (
"github.com/spf13/cobra"
)
-func newInternalCommand() *cobra.Command {
+func NewInternalCommand() *cobra.Command {
var internalCommand = &cobra.Command{
Use: "internal",
Short: "DO NOT EXECUTE MANUALLY",
diff --git a/cmd/nerdctl/internal_oci_hook.go b/cmd/nerdctl/internal/internal_oci_hook.go
similarity index 84%
rename from cmd/nerdctl/internal_oci_hook.go
rename to cmd/nerdctl/internal/internal_oci_hook.go
index e75b32b66a7..31c8e5b910d 100644
--- a/cmd/nerdctl/internal_oci_hook.go
+++ b/cmd/nerdctl/internal/internal_oci_hook.go
@@ -14,16 +14,17 @@
limitations under the License.
*/
-package main
+package internal
import (
"errors"
"os"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/ocihook"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/ocihook"
)
func newInternalOCIHookCommandCommand() *cobra.Command {
@@ -38,7 +39,7 @@ func newInternalOCIHookCommandCommand() *cobra.Command {
}
func internalOCIHookAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -55,9 +56,11 @@ func internalOCIHookAction(cmd *cobra.Command, args []string) error {
}
cniPath := globalOptions.CNIPath
cniNetconfpath := globalOptions.CNINetConfPath
+ bridgeIP := globalOptions.BridgeIP
return ocihook.Run(os.Stdin, os.Stderr, event,
dataStore,
cniPath,
cniNetconfpath,
+ bridgeIP,
)
}
diff --git a/cmd/nerdctl/ipfs.go b/cmd/nerdctl/ipfs/ipfs.go
similarity index 78%
rename from cmd/nerdctl/ipfs.go
rename to cmd/nerdctl/ipfs/ipfs.go
index 871077e1e5f..e6e3a2194c8 100644
--- a/cmd/nerdctl/ipfs.go
+++ b/cmd/nerdctl/ipfs/ipfs.go
@@ -14,18 +14,20 @@
limitations under the License.
*/
-package main
+package ipfs
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newIPFSCommand() *cobra.Command {
+func NewIPFSCommand() *cobra.Command {
cmd := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "ipfs",
Short: "Distributing images on IPFS",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go
new file mode 100644
index 00000000000..e1b78cc4c1e
--- /dev/null
+++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go
@@ -0,0 +1,326 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipfs
+
+import (
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/portlock"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestIPFSCompNoBuild(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const ipfsAddrKey = "ipfsAddrKey"
+
+ var ipfsRegistry *registry.Server
+
+ testCase.Require = test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.Registry,
+ nerdtest.IPFS,
+ nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/3510"),
+ // See note below
+ // nerdtest.Private,
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ // Start Kubo
+ ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil)
+ ipfsRegistry.Setup(data, helpers)
+ data.Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port))
+
+ // Ensure we have the images
+ helpers.Ensure("pull", "--quiet", testutil.WordpressImage)
+ helpers.Ensure("pull", "--quiet", testutil.MariaDBImage)
+ }
+
+ testCase.SubTests = []*test.Case{
+ subtestTestIPFSCompNoB(t, false, false),
+ subtestTestIPFSCompNoB(t, true, false),
+ subtestTestIPFSCompNoB(t, false, true),
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ if ipfsRegistry != nil {
+ ipfsRegistry.Cleanup(data, helpers)
+ }
+ helpers.Anyhow("rmi", "-f", testutil.WordpressImage)
+ helpers.Anyhow("rmi", "-f", testutil.MariaDBImage)
+ }
+
+ testCase.Run(t)
+}
+
+func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case {
+ t.Helper()
+
+ const ipfsAddrKey = "ipfsAddrKey"
+ const mariaImageCIDKey = "mariaImageCIDKey"
+ const wordpressImageCIDKey = "wordpressImageCIDKey"
+ const composeExtraKey = "composeExtraKey"
+
+ testCase := &test.Case{}
+
+ testCase.Description += "with"
+
+ if !stargz {
+ testCase.Description += "-no"
+ }
+ testCase.Description += "-stargz"
+
+ if !byAddr {
+ testCase.Description += "-no"
+ }
+ testCase.Description += "-byAddr"
+
+ if stargz {
+ testCase.Require = nerdtest.Stargz
+ }
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ var ipfsCIDWP, ipfsCIDMD string
+ if stargz {
+ ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--estargz")
+ ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--estargz")
+ } else if byAddr {
+ ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Get(ipfsAddrKey))
+ ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Get(ipfsAddrKey))
+ data.Set(composeExtraKey, "--ipfs-address="+data.Get(ipfsAddrKey))
+ } else {
+ ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage)
+ ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage)
+ }
+ data.Set(wordpressImageCIDKey, ipfsCIDWP)
+ data.Set(mariaImageCIDKey, ipfsCIDMD)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ // NOTE:
+ // Removing these images locally forces tests to be sequentials (as IPFS being content addressable,
+ // they have the same cid - except for the estargz version obviously)
+ // Deliberately electing to not remove them here so that we can parallelize and cut down the running time
+ /*
+ if data.Get(mariaImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mariaImageCIDKey))
+ helpers.Anyhow("rmi", "-f", data.Get(wordpressImageCIDKey))
+ }
+ */
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ safePort, err := portlock.Acquire(0)
+ assert.NilError(helpers.T(), err)
+ data.Set("wordpressPort", strconv.Itoa(safePort))
+ composeUP(data, helpers, fmt.Sprintf(`
+version: '3.1'
+
+services:
+
+ wordpress:
+ image: ipfs://%s
+ restart: always
+ ports:
+ - %d:80
+ environment:
+ WORDPRESS_DB_HOST: db
+ WORDPRESS_DB_USER: exampleuser
+ WORDPRESS_DB_PASSWORD: examplepass
+ WORDPRESS_DB_NAME: exampledb
+ # FIXME: this is flaky and will make the container fail on occasions
+ volumes:
+ - wordpress:/var/www/html
+
+ db:
+ image: ipfs://%s
+ restart: always
+ environment:
+ MYSQL_DATABASE: exampledb
+ MYSQL_USER: exampleuser
+ MYSQL_PASSWORD: examplepass
+ MYSQL_RANDOM_ROOT_PASSWORD: '1'
+ volumes:
+ - db:/var/lib/mysql
+
+volumes:
+ wordpress:
+ db:
+`, data.Get(wordpressImageCIDKey), safePort, data.Get(mariaImageCIDKey)), data.Get(composeExtraKey))
+ // FIXME: need to break down composeUP into testable commands instead
+ // Right now, this is just a dummy placeholder
+ return helpers.Command("info")
+ }
+
+ testCase.Expected = test.Expects(0, nil, nil)
+
+ return testCase
+}
+
+func TestIPFSCompBuild(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ var ipfsServer test.TestableCommand
+ var comp *testutil.ComposeDir
+
+ const mainImageCIDKey = "mainImageCIDKey"
+ safePort, err := portlock.Acquire(0)
+ assert.NilError(t, err)
+ var listenAddr = "localhost:" + strconv.Itoa(safePort)
+
+ testCase.Require = test.Require(
+ // Linux only
+ test.Linux,
+ // Obviously not docker supported
+ test.Not(nerdtest.Docker),
+ nerdtest.Build,
+ nerdtest.IPFS,
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ // Get alpine
+ helpers.Ensure("pull", "--quiet", testutil.NginxAlpineImage)
+ // Start a local ipfs backed registry
+ // FIXME: this is bad and likely to collide with other tests
+ ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr)
+ // Once foregrounded, do not wait for it more than a second
+ ipfsServer.Background(1 * time.Second)
+ // Apparently necessary to let it start...
+ time.Sleep(time.Second)
+
+ // Save nginx to ipfs
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage))
+
+ const dockerComposeYAML = `
+services:
+ web:
+ build: .
+ ports:
+ - 8081:80
+`
+ dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s
+COPY index.html /usr/share/nginx/html/index.html
+`, listenAddr, data.Get(mainImageCIDKey))
+
+ comp = testutil.NewComposeDir(t, dockerComposeYAML)
+ comp.WriteFile("Dockerfile", dockerfile)
+ comp.WriteFile("index.html", data.Identifier("indexhtml"))
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ if ipfsServer != nil {
+ // Close the server once done
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ ipfsServer.Run(nil)
+ }
+ if comp != nil {
+ helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v")
+ comp.CleanUp()
+ }
+ }
+
+ testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("compose", "-f", comp.YAMLFullPath(), "up", "-d", "--build")
+ }
+
+ testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ resp, err := nettestutil.HTTPGet("http://127.0.0.1:8081", 10, false)
+ assert.NilError(t, err)
+ respBody, err := io.ReadAll(resp.Body)
+ assert.NilError(t, err)
+ t.Logf("respBody=%q", respBody)
+ assert.Assert(t, strings.Contains(string(respBody), data.Identifier("indexhtml")))
+ },
+ }
+ }
+
+ testCase.Run(t)
+}
+
+func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, opts string) {
+ comp := testutil.NewComposeDir(helpers.T(), dockerComposeYAML)
+ // defer comp.CleanUp()
+
+ // Because it might or might not happen, and
+ helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v")
+ defer helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v")
+
+ projectName := comp.ProjectName()
+
+ args := []string{"compose", "-f", comp.YAMLFullPath()}
+ if opts != "" {
+ args = append(args, opts)
+ }
+
+ helpers.Ensure(append(args, "up", "--quiet-pull", "-d")...)
+
+ helpers.Ensure("volume", "inspect", fmt.Sprintf("%s_db", projectName))
+ helpers.Ensure("network", "inspect", fmt.Sprintf("%s_default", projectName))
+
+ checkWordpress := func() error {
+ // FIXME: see other notes on using the same port repeatedly
+ resp, err := nettestutil.HTTPGet("http://127.0.0.1:"+data.Get("wordpressPort"), 5, false)
+ if err != nil {
+ return err
+ }
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ if !strings.Contains(string(respBody), testutil.WordpressIndexHTMLSnippet) {
+ return fmt.Errorf("respBody does not contain %q (%s)", testutil.WordpressIndexHTMLSnippet, string(respBody))
+ }
+ return nil
+ }
+
+ var wordpressWorking bool
+ var err error
+ // 15 seconds is long enough
+ for i := 0; i < 5; i++ {
+ err = checkWordpress()
+ if err == nil {
+ wordpressWorking = true
+ break
+ }
+ time.Sleep(3 * time.Second)
+ }
+
+ if !wordpressWorking {
+ ccc := helpers.Capture("ps", "-a")
+ helpers.T().Log(ccc)
+ helpers.T().Error(helpers.Err("logs", projectName+"-wordpress-1"))
+ helpers.T().Fatalf("wordpress is not working %v", err)
+ }
+
+ helpers.Ensure("compose", "-f", comp.YAMLFullPath(), "down", "-v")
+ helpers.Fail("volume", "inspect", fmt.Sprintf("%s_db", projectName))
+ helpers.Fail("network", "inspect", fmt.Sprintf("%s_default", projectName))
+}
diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go
new file mode 100644
index 00000000000..cb21a839779
--- /dev/null
+++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go
@@ -0,0 +1,105 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipfs
+
+import (
+ "fmt"
+ "regexp"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestIPFSAddrWithKubo(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const mainImageCIDKey = "mainImagemainImageCIDKey"
+ const ipfsAddrKey = "ipfsAddrKey"
+
+ var ipfsRegistry *registry.Server
+
+ testCase.Require = test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.Registry,
+ nerdtest.Private,
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+
+ ipfsRegistry = registry.NewKuboRegistry(data, helpers, t, nil, 0, nil)
+ ipfsRegistry.Setup(data, helpers)
+ ipfsAddr := fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)
+ data.Set(ipfsAddrKey, ipfsAddr)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ if ipfsRegistry != nil {
+ ipfsRegistry.Cleanup(data, helpers)
+ }
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "with default snapshotter",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey)))
+ helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID)
+ data.Set(mainImageCIDKey, ipfsCID)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello")
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello\n")),
+ },
+ {
+ Description: "with stargz snapshotter",
+ NoParallel: true,
+ Require: test.Require(
+ nerdtest.Stargz,
+ nerdtest.Private,
+ nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Get(ipfsAddrKey)), "--estargz")
+ helpers.Ensure("pull", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID)
+ data.Set(mainImageCIDKey, ipfsCID)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter")
+ },
+ Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/ipfs_registry.go b/cmd/nerdctl/ipfs/ipfs_registry.go
similarity index 78%
rename from cmd/nerdctl/ipfs_registry.go
rename to cmd/nerdctl/ipfs/ipfs_registry.go
index 3211ad6ed8c..9beb019f8f3 100644
--- a/cmd/nerdctl/ipfs_registry.go
+++ b/cmd/nerdctl/ipfs/ipfs_registry.go
@@ -14,19 +14,21 @@
limitations under the License.
*/
-package main
+package ipfs
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
func newIPFSRegistryCommand() *cobra.Command {
cmd := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "registry",
Short: "Manage read-only registry backed by IPFS",
- PreRunE: checkExperimental("ipfs"),
- RunE: unknownSubcommandAction,
+ PreRunE: helpers.CheckExperimental("ipfs"),
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go
new file mode 100644
index 00000000000..3b4f61183e8
--- /dev/null
+++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go
@@ -0,0 +1,150 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipfs
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func pushToIPFS(helpers test.Helpers, name string, opts ...string) string {
+ var ipfsCID string
+ cmd := helpers.Command("push", "ipfs://"+name)
+ cmd.WithArgs(opts...)
+ cmd.Run(&test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ lines := strings.Split(stdout, "\n")
+ assert.Equal(t, len(lines) >= 2, true)
+ ipfsCID = lines[len(lines)-2]
+ },
+ })
+ return ipfsCID
+}
+
+func TestIPFSNerdctlRegistry(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // FIXME: this is bad and likely to collide with other tests
+ const listenAddr = "localhost:5555"
+
+ const ipfsImageURLKey = "ipfsImageURLKey"
+
+ var ipfsServer test.TestableCommand
+
+ testCase.Require = test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.IPFS,
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+
+ // Start a local ipfs backed registry
+ ipfsServer = helpers.Command("ipfs", "registry", "serve", "--listen-registry", listenAddr)
+ // Once foregrounded, do not wait for it more than a second
+ ipfsServer.Background(1 * time.Second)
+ // Apparently necessary to let it start...
+ time.Sleep(time.Second)
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ if ipfsServer != nil {
+ // Close the server once done
+ ipfsServer.Run(nil)
+ }
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "with default snapshotter",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage))
+ helpers.Ensure("pull", "--quiet", data.Get(ipfsImageURLKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(ipfsImageURLKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(ipfsImageURLKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "echo", "hello")
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello\n")),
+ },
+ {
+ Description: "with stargz snapshotterr",
+ NoParallel: true,
+ Require: nerdtest.Stargz,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage, "--estargz"))
+ helpers.Ensure("pull", "--quiet", data.Get(ipfsImageURLKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(ipfsImageURLKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(ipfsImageURLKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter")
+ },
+ Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))),
+ },
+ {
+ Description: "with build",
+ NoParallel: true,
+ Require: nerdtest.Build,
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rmi", "-f", data.Identifier("built-image"))
+ if data.Get(ipfsImageURLKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(ipfsImageURLKey))
+ }
+ },
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage))
+
+ dockerfile := fmt.Sprintf(`FROM %s
+CMD ["echo", "nerdctl-build-test-string"]
+ `, data.Get(ipfsImageURLKey))
+
+ buildCtx := data.TempDir()
+ err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600)
+ assert.NilError(helpers.T(), err)
+
+ helpers.Ensure("build", "-t", data.Identifier("built-image"), buildCtx)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Identifier("built-image"))
+ },
+ Expected: test.Expects(0, nil, test.Equals("nerdctl-build-test-string\n")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/ipfs_registry_serve.go b/cmd/nerdctl/ipfs/ipfs_registry_serve.go
similarity index 68%
rename from cmd/nerdctl/ipfs_registry_serve.go
rename to cmd/nerdctl/ipfs/ipfs_registry_serve.go
index 81708d2c56e..739d3d3ece6 100644
--- a/cmd/nerdctl/ipfs_registry_serve.go
+++ b/cmd/nerdctl/ipfs/ipfs_registry_serve.go
@@ -14,12 +14,14 @@
limitations under the License.
*/
-package main
+package ipfs
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/ipfs"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/ipfs"
)
const (
@@ -37,10 +39,10 @@ func newIPFSRegistryServeCommand() *cobra.Command {
SilenceErrors: true,
}
- AddStringFlag(ipfsRegistryServeCommand, "listen-registry", nil, defaultIPFSRegistry, "IPFS_REGISTRY_SERVE_LISTEN_REGISTRY", "address to listen")
- AddStringFlag(ipfsRegistryServeCommand, "ipfs-address", nil, "", "IPFS_REGISTRY_SERVE_IPFS_ADDRESS", "multiaddr of IPFS API (default is pulled from $IPFS_PATH/api file. If $IPFS_PATH env var is not present, it defaults to ~/.ipfs)")
- AddIntFlag(ipfsRegistryServeCommand, "read-retry-num", nil, defaultIPFSReadRetryNum, "IPFS_REGISTRY_SERVE_READ_RETRY_NUM", "times to retry query on IPFS. Zero or lower means no retry.")
- AddDurationFlag(ipfsRegistryServeCommand, "read-timeout", nil, defaultIPFSReadTimeoutDuration, "IPFS_REGISTRY_SERVE_READ_TIMEOUT", "timeout duration of a read request to IPFS. Zero means no timeout.")
+ helpers.AddStringFlag(ipfsRegistryServeCommand, "listen-registry", nil, defaultIPFSRegistry, "IPFS_REGISTRY_SERVE_LISTEN_REGISTRY", "address to listen")
+ helpers.AddStringFlag(ipfsRegistryServeCommand, "ipfs-address", nil, "", "IPFS_REGISTRY_SERVE_IPFS_ADDRESS", "multiaddr of IPFS API (default is pulled from $IPFS_PATH/api file. If $IPFS_PATH env var is not present, it defaults to ~/.ipfs)")
+ helpers.AddIntFlag(ipfsRegistryServeCommand, "read-retry-num", nil, defaultIPFSReadRetryNum, "IPFS_REGISTRY_SERVE_READ_RETRY_NUM", "times to retry query on IPFS. Zero or lower means no retry.")
+ helpers.AddDurationFlag(ipfsRegistryServeCommand, "read-timeout", nil, defaultIPFSReadTimeoutDuration, "IPFS_REGISTRY_SERVE_READ_TIMEOUT", "timeout duration of a read request to IPFS. Zero means no timeout.")
return ipfsRegistryServeCommand
}
diff --git a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go
new file mode 100644
index 00000000000..48bc874049b
--- /dev/null
+++ b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go
@@ -0,0 +1,229 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipfs
+
+import (
+ "regexp"
+ "testing"
+
+ testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestIPFSSimple(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const mainImageCIDKey = "mainImageCIDKey"
+ const transformedImageCIDKey = "transformedImageCIDKey"
+
+ testCase.Require = test.Require(
+ test.Linux,
+ test.Not(nerdtest.Docker),
+ nerdtest.IPFS,
+ // We constantly rmi the image by its CID which is shared across tests, so, we make this group private
+ // and every subtest NoParallel
+ nerdtest.Private,
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("pull", "--quiet", testutil.CommonImage)
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "with default snapshotter",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage))
+ helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello")
+ },
+ Expected: test.Expects(0, nil, test.Equals("hello\n")),
+ },
+ {
+ Description: "with stargz snapshotter",
+ NoParallel: true,
+ Require: test.Require(
+ nerdtest.Stargz,
+ nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz"))
+ helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ }
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter")
+ },
+ Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))),
+ },
+ {
+ Description: "with commit and push",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage))
+ helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey))
+
+ // Run a container that does modify something, then commit and push it
+ helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello")
+ helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image"))
+ data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image")))
+
+ // Clean-up
+ helpers.Ensure("rm", data.Identifier("commit-container"))
+ helpers.Ensure("rmi", data.Identifier("commit-image"))
+
+ // Pull back the committed image
+ helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("commit-container"))
+ helpers.Anyhow("rmi", "-f", data.Identifier("commit-image"))
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey))
+ }
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "cat", "/hello")
+ },
+
+ Expected: test.Expects(0, nil, test.Equals("hello\n")),
+ },
+ {
+ Description: "with commit and push, stargz lazy pulling",
+ NoParallel: true,
+ Require: test.Require(
+ nerdtest.Stargz,
+ nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3475"),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz"))
+ helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey))
+
+ // Run a container that does modify something, then commit and push it
+ helpers.Ensure("run", "--name", data.Identifier("commit-container"), data.Get(mainImageCIDKey), "sh", "-c", "--", "echo hello > /hello")
+ helpers.Ensure("commit", data.Identifier("commit-container"), data.Identifier("commit-image"))
+ data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("commit-image")))
+
+ // Clean-up
+ helpers.Ensure("rm", data.Identifier("commit-container"))
+ helpers.Ensure("rmi", data.Identifier("commit-image"))
+
+ // Pull back the image
+ helpers.Ensure("pull", "ipfs://"+data.Get(transformedImageCIDKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("commit-container"))
+ helpers.Anyhow("rmi", "-f", data.Identifier("commit-image"))
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey))
+ }
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter")
+ },
+
+ Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("hello[\n]sha256:.*[.]json[\n]"))),
+ },
+ {
+ Description: "with encryption",
+ NoParallel: true,
+ Require: test.Binary("openssl"),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage))
+ helpers.Ensure("pull", "ipfs://"+data.Get(mainImageCIDKey))
+
+ // Prep a key pair
+ keyPair := testhelpers.NewJWEKeyPair(t)
+ // FIXME: this will only cleanup when the group is done, not right, but it works
+ t.Cleanup(keyPair.Cleanup)
+ data.Set("pub", keyPair.Pub)
+ data.Set("prv", keyPair.Prv)
+
+ // Encrypt the image, and verify it is encrypted
+ helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, data.Get(mainImageCIDKey), data.Identifier("encrypted"))
+ cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted"))
+ cmd.Run(&test.Expected{
+ ExitCode: 1,
+ Output: test.Equals("1\n"),
+ })
+ cmd = helpers.Command("image", "inspect", "--mode=native", "--format={{json (index .Manifest.Layers 0) }}", data.Identifier("encrypted"))
+ cmd.Run(&test.Expected{
+ ExitCode: 1,
+ Output: test.Contains("org.opencontainers.image.enc.keys.jwe"),
+ })
+
+ // Push the encrypted image and save the CID
+ data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted")))
+
+ // Remove both images locally
+ helpers.Ensure("rmi", "-f", data.Get(mainImageCIDKey))
+ helpers.Ensure("rmi", "-f", data.Get(transformedImageCIDKey))
+
+ // Pull back without unpacking
+ helpers.Ensure("pull", "--unpack=false", "ipfs://"+data.Get(transformedImageCIDKey))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get(mainImageCIDKey) != "" {
+ helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey))
+ helpers.Anyhow("rmi", "-f", data.Get(transformedImageCIDKey))
+ }
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "decrypt with pub key does not work",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("decrypted"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "decrypt", "--key="+data.Get("pub"), data.Get(transformedImageCIDKey), data.Identifier("decrypted"))
+ },
+ Expected: test.Expects(1, nil, nil),
+ },
+ {
+ Description: "decrypt with priv key does work",
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier("decrypted"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "decrypt", "--key="+data.Get("prv"), data.Get(transformedImageCIDKey), data.Identifier("decrypted"))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/ipfs/ipfs_test.go b/cmd/nerdctl/ipfs/ipfs_test.go
new file mode 100644
index 00000000000..1b6749bee5f
--- /dev/null
+++ b/cmd/nerdctl/ipfs/ipfs_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipfs
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/ipfs_build_linux_test.go b/cmd/nerdctl/ipfs_build_linux_test.go
deleted file mode 100644
index e7af2400786..00000000000
--- a/cmd/nerdctl/ipfs_build_linux_test.go
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "strings"
- "testing"
- "time"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-
- "gotest.tools/v3/assert"
- "gotest.tools/v3/icmd"
-)
-
-func TestIPFSBuild(t *testing.T) {
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage)
- ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://")
-
- imageName := testutil.Identifier(t)
- defer base.Cmd("rmi", imageName).Run()
-
- dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s
-CMD ["echo", "nerdctl-build-test-string"]
- `, ipfsCIDBase)
-
- buildCtx, err := createBuildContext(dockerfile)
- assert.NilError(t, err)
- defer os.RemoveAll(buildCtx)
-
- done := ipfsRegistryUp(t, base)
- defer done()
- base.Cmd("build", "-t", imageName, buildCtx).AssertOK()
- base.Cmd("build", buildCtx, "-t", imageName).AssertOK()
-
- base.Cmd("run", "--rm", imageName).AssertOutContains("nerdctl-build-test-string")
-}
-
-func ipfsRegistryUp(t *testing.T, base *testutil.Base, args ...string) (done func() error) {
- res := icmd.StartCmd(base.Cmd(append([]string{"ipfs", "registry", "serve"}, args...)...).Cmd)
- time.Sleep(time.Second)
- assert.Assert(t, res.Cmd.Process != nil)
- assert.NilError(t, res.Error)
- return func() error {
- res.Cmd.Process.Kill()
- icmd.WaitOnCmd(3*time.Second, res)
- return nil
- }
-}
diff --git a/cmd/nerdctl/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs_compose_linux_test.go
deleted file mode 100644
index 087fbed2e8a..00000000000
--- a/cmd/nerdctl/ipfs_compose_linux_test.go
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "io"
- "os"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/nettestutil"
- "gotest.tools/v3/assert"
-)
-
-func TestIPFSComposeUp(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- ipfsaddr, done := runIPFSDaemonContainer(t, base)
- defer done()
- tests := []struct {
- name string
- snapshotter string
- pushOptions []string
- composeOptions []string
- requiresStargz bool
- }{
- {
- name: "overlayfs",
- snapshotter: "overlayfs",
- },
- {
- name: "stargz",
- snapshotter: "stargz",
- pushOptions: []string{"--estargz"},
- requiresStargz: true,
- },
- {
- name: "ipfs-address",
- snapshotter: "overlayfs",
- pushOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)},
- composeOptions: []string{fmt.Sprintf("--ipfs-address=%s", ipfsaddr)},
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- base := testutil.NewBase(t)
- if tt.requiresStargz {
- requiresStargz(base)
- }
- ipfsImgs := make([]string, 2)
- for i, img := range []string{testutil.WordpressImage, testutil.MariaDBImage} {
- ipfsImgs[i] = pushImageToIPFS(t, base, img, tt.pushOptions...)
- }
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER="+tt.snapshotter)
- testComposeUp(t, base, fmt.Sprintf(`
-version: '3.1'
-
-services:
-
- wordpress:
- image: %s
- restart: always
- ports:
- - 8080:80
- environment:
- WORDPRESS_DB_HOST: db
- WORDPRESS_DB_USER: exampleuser
- WORDPRESS_DB_PASSWORD: examplepass
- WORDPRESS_DB_NAME: exampledb
- volumes:
- # workaround for https://github.com/containerd/stargz-snapshotter/issues/444
- - "/run"
- - wordpress:/var/www/html
-
- db:
- image: %s
- restart: always
- environment:
- MYSQL_DATABASE: exampledb
- MYSQL_USER: exampleuser
- MYSQL_PASSWORD: examplepass
- MYSQL_RANDOM_ROOT_PASSWORD: '1'
- volumes:
- # workaround for https://github.com/containerd/stargz-snapshotter/issues/444
- - "/run"
- - db:/var/lib/mysql
-
-volumes:
- wordpress:
- db:
-`, ipfsImgs[0], ipfsImgs[1]), tt.composeOptions...)
- })
- }
-}
-
-func TestIPFSComposeUpBuild(t *testing.T) {
- testutil.DockerIncompatible(t)
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- defer base.Cmd("builder", "prune").Run()
- ipfsCID := pushImageToIPFS(t, base, testutil.NginxAlpineImage)
- ipfsCIDBase := strings.TrimPrefix(ipfsCID, "ipfs://")
-
- const dockerComposeYAML = `
-services:
- web:
- build: .
- ports:
- - 8080:80
-`
- dockerfile := fmt.Sprintf(`FROM localhost:5050/ipfs/%s
-COPY index.html /usr/share/nginx/html/index.html
-`, ipfsCIDBase)
- indexHTML := t.Name()
-
- comp := testutil.NewComposeDir(t, dockerComposeYAML)
- defer comp.CleanUp()
-
- comp.WriteFile("Dockerfile", dockerfile)
- comp.WriteFile("index.html", indexHTML)
-
- done := ipfsRegistryUp(t, base)
- defer done()
- base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK()
- defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()
-
- resp, err := nettestutil.HTTPGet("http://127.0.0.1:8080", 50, false)
- assert.NilError(t, err)
- respBody, err := io.ReadAll(resp.Body)
- assert.NilError(t, err)
- t.Logf("respBody=%q", respBody)
- assert.Assert(t, strings.Contains(string(respBody), indexHTML))
-}
diff --git a/cmd/nerdctl/ipfs_linux_test.go b/cmd/nerdctl/ipfs_linux_test.go
deleted file mode 100644
index 523d5eb981f..00000000000
--- a/cmd/nerdctl/ipfs_linux_test.go
+++ /dev/null
@@ -1,162 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "os"
- "regexp"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
-
- "gotest.tools/v3/assert"
-)
-
-func TestIPFS(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage)
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
- base.Cmd("pull", ipfsCID).AssertOK()
- base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK()
-
- // encryption
- keyPair := newJWEKeyPair(t)
- defer keyPair.cleanup()
- tID := testutil.Identifier(t)
- encryptImageRef := tID + ":enc"
- layersNum := 1
- base.Cmd("image", "encrypt", "--recipient=jwe:"+keyPair.pub, ipfsCID, encryptImageRef).AssertOK()
- base.Cmd("image", "inspect", "--mode=native", "--format={{len .Manifest.Layers}}", encryptImageRef).AssertOutExactly(fmt.Sprintf("%d\n", layersNum))
- for i := 0; i < layersNum; i++ {
- base.Cmd("image", "inspect", "--mode=native", fmt.Sprintf("--format={{json (index .Manifest.Layers %d) }}", i), encryptImageRef).AssertOutContains("org.opencontainers.image.enc.keys.jwe")
- }
- ipfsCIDEnc := cidOf(t, base.Cmd("push", "ipfs://"+encryptImageRef).OutLines())
- rmiAll(base)
-
- decryptImageRef := tID + ":dec"
- base.Cmd("pull", "--unpack=false", ipfsCIDEnc).AssertOK()
- base.Cmd("image", "decrypt", "--key="+keyPair.pub, ipfsCIDEnc, decryptImageRef).AssertFail() // decryption needs prv key, not pub key
- base.Cmd("image", "decrypt", "--key="+keyPair.prv, ipfsCIDEnc, decryptImageRef).AssertOK()
- base.Cmd("run", "--rm", decryptImageRef, "/bin/sh", "-c", "echo hello").AssertOK()
-}
-
-var iplineRegexp = regexp.MustCompile(`"([0-9\.]*)"`)
-
-func TestIPFSAddress(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- ipfsaddr, done := runIPFSDaemonContainer(t, base)
- defer done()
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, fmt.Sprintf("--ipfs-address=%s", ipfsaddr))
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
- base.Cmd("pull", "--ipfs-address", ipfsaddr, ipfsCID).AssertOK()
- base.Cmd("run", "--ipfs-address", ipfsaddr, "--rm", ipfsCID, "echo", "hello").AssertOK()
-}
-
-func runIPFSDaemonContainer(t *testing.T, base *testutil.Base) (ipfsAddress string, done func()) {
- name := "test-ipfs-address"
- base.Cmd("run", "-d", "--name", name, "--entrypoint=/bin/sh", testutil.KuboImage, "-c", "ipfs init && ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001 && ipfs daemon --offline").AssertOK()
- iplines := base.Cmd("inspect", name, "-f", "'{{json .NetworkSettings.IPAddress}}'").OutLines()
- t.Logf("IPAddress=%v", iplines)
- assert.Equal(t, len(iplines), 2)
- matches := iplineRegexp.FindStringSubmatch(iplines[0])
- t.Logf("ip address matches=%v", matches)
- assert.Equal(t, len(matches), 2)
- ipfsaddr := fmt.Sprintf("/ip4/%s/tcp/5001", matches[1])
- return ipfsaddr, func() {
- base.Cmd("kill", "test-ipfs-address").AssertOK()
- base.Cmd("rm", "test-ipfs-address").AssertOK()
- }
-}
-
-func TestIPFSCommit(t *testing.T) {
- // cgroup is required for nerdctl commit
- if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
- t.Skip("test skipped for rootless containers on cgroup v1")
- }
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage)
-
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
- base.Cmd("pull", ipfsCID).AssertOK()
- base.Cmd("run", "--rm", ipfsCID, "echo", "hello").AssertOK()
- tID := testutil.Identifier(t)
- newContainer, newImg := tID, tID+":v1"
- base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK()
- base.Cmd("commit", newContainer, newImg).AssertOK()
- base.Cmd("kill", newContainer).AssertOK()
- base.Cmd("rm", newContainer).AssertOK()
- ipfsCID2 := cidOf(t, base.Cmd("push", "ipfs://"+newImg).OutLines())
- rmiAll(base)
- base.Cmd("pull", ipfsCID2).AssertOK()
- base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "cat /hello").AssertOK()
-}
-
-func TestIPFSWithLazyPulling(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- requiresStargz(base)
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz")
-
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz")
- base.Cmd("pull", ipfsCID).AssertOK()
- base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK()
-}
-
-func TestIPFSWithLazyPullingCommit(t *testing.T) {
- // cgroup is required for nerdctl commit
- if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
- t.Skip("test skipped for rootless containers on cgroup v1")
- }
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- requiresStargz(base)
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz")
-
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz")
- base.Cmd("pull", ipfsCID).AssertOK()
- base.Cmd("run", "--rm", ipfsCID, "ls", "/.stargz-snapshotter").AssertOK()
- tID := testutil.Identifier(t)
- newContainer, newImg := tID, tID+":v1"
- base.Cmd("run", "--name", newContainer, "-d", ipfsCID, "/bin/sh", "-c", "echo hello > /hello ; sleep 10000").AssertOK()
- base.Cmd("commit", newContainer, newImg).AssertOK()
- base.Cmd("kill", newContainer).AssertOK()
- base.Cmd("rm", newContainer).AssertOK()
- ipfsCID2 := cidOf(t, base.Cmd("push", "--estargz", "ipfs://"+newImg).OutLines())
- rmiAll(base)
-
- base.Cmd("pull", ipfsCID2).AssertOK()
- base.Cmd("run", "--rm", ipfsCID2, "/bin/sh", "-c", "ls /.stargz-snapshotter && cat /hello").AssertOK()
- base.Cmd("image", "rm", ipfsCID2).AssertOK()
-}
-
-func pushImageToIPFS(t *testing.T, base *testutil.Base, name string, opts ...string) string {
- base.Cmd("pull", name).AssertOK()
- ipfsCID := cidOf(t, base.Cmd(append([]string{"push"}, append(opts, "ipfs://"+name)...)...).OutLines())
- base.Cmd("rmi", name).AssertOK()
- return ipfsCID
-}
-
-func cidOf(t *testing.T, lines []string) string {
- assert.Equal(t, len(lines) >= 2, true)
- return "ipfs://" + lines[len(lines)-2]
-}
diff --git a/cmd/nerdctl/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs_registry_linux_test.go
deleted file mode 100644
index cec59e59756..00000000000
--- a/cmd/nerdctl/ipfs_registry_linux_test.go
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "os"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestIPFSRegistry(t *testing.T) {
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=overlayfs")
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage)
- ipfsRegistryAddr := "localhost:5555"
- ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID)
-
- done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr)
- defer done()
- base.Cmd("pull", ipfsRegistryRef).AssertOK()
- base.Cmd("run", "--rm", ipfsRegistryRef, "echo", "hello").AssertOK()
-}
-
-func TestIPFSRegistryWithLazyPulling(t *testing.T) {
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- requiresStargz(base)
- base.Env = append(os.Environ(), "CONTAINERD_SNAPSHOTTER=stargz")
- ipfsCID := pushImageToIPFS(t, base, testutil.AlpineImage, "--estargz")
- ipfsRegistryAddr := "localhost:5555"
- ipfsRegistryRef := ipfsRegistryReference(ipfsRegistryAddr, ipfsCID)
-
- done := ipfsRegistryUp(t, base, "--listen-registry", ipfsRegistryAddr)
- defer done()
- base.Cmd("pull", ipfsRegistryRef).AssertOK()
- base.Cmd("run", "--rm", ipfsRegistryRef, "ls", "/.stargz-snapshotter").AssertOK()
-}
-
-func ipfsRegistryReference(addr string, c string) string {
- return addr + "/ipfs/" + strings.TrimPrefix(c, "ipfs://")
-}
diff --git a/cmd/nerdctl/issues/issues_linux_test.go b/cmd/nerdctl/issues/issues_linux_test.go
new file mode 100644
index 00000000000..3f9e89ffd89
--- /dev/null
+++ b/cmd/nerdctl/issues/issues_linux_test.go
@@ -0,0 +1,155 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package issues is meant to document testing for complex scenarios type of issues that cannot simply be ascribed
+// to a specific package.
+package issues
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestIssue3425(t *testing.T) {
+ nerdtest.Setup()
+
+ var reg *registry.Server
+
+ testCase := &test.Case{
+ Require: nerdtest.Registry,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false)
+ reg.Setup(data, helpers)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if reg != nil {
+ reg.Cleanup(data, helpers)
+ }
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "with tag",
+ Require: nerdtest.Private,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage)
+ helpers.Ensure("image", "rm", "-f", testutil.CommonImage)
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("tag", testutil.CommonImage, fmt.Sprintf("localhost:%d/%s", reg.Port, identifier))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("rm", "-f", identifier)
+ helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", reg.Port, identifier))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier()))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with commit",
+ Require: nerdtest.Private,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "touch", "/something")
+ helpers.Ensure("image", "rm", "-f", testutil.CommonImage)
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("commit", identifier, fmt.Sprintf("localhost:%d/%s", reg.Port, identifier))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("rmi", "-f", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier()))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("push", fmt.Sprintf("localhost:%d/%s", reg.Port, data.Identifier()))
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with save",
+ Require: nerdtest.Private,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage)
+ helpers.Ensure("image", "rm", "-f", testutil.CommonImage)
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("save", testutil.CommonImage)
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with convert",
+ Require: test.Require(
+ nerdtest.Private,
+ test.Not(test.Windows),
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage)
+ helpers.Ensure("image", "rm", "-f", testutil.CommonImage)
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "convert", "--oci", "--estargz", testutil.CommonImage, data.Identifier())
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "with ipfs",
+ Require: test.Require(
+ nerdtest.Private,
+ nerdtest.IPFS,
+ test.Not(test.Windows),
+ test.Not(nerdtest.Docker),
+ ),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage)
+ helpers.Ensure("image", "rm", "-f", testutil.CommonImage)
+ helpers.Ensure("image", "pull", testutil.CommonImage)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("rmi", "-f", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("image", "push", "ipfs://"+testutil.CommonImage)
+ },
+ Expected: test.Expects(0, nil, nil),
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/issues/main_linux_test.go b/cmd/nerdctl/issues/main_linux_test.go
new file mode 100644
index 00000000000..a1af21d8ff5
--- /dev/null
+++ b/cmd/nerdctl/issues/main_linux_test.go
@@ -0,0 +1,58 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package issues
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
+
+// TestIssue108 tests https://github.com/containerd/nerdctl/issues/108
+// ("`nerdctl run --net=host -it` fails while `nerdctl run -it --net=host` works")
+func TestIssue108(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "-it --net=host",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("run", "-it", "--rm", "--net=host", testutil.CommonImage, "echo", "this was always working")
+ cmd.WithPseudoTTY()
+ return cmd
+ },
+ Expected: test.Expects(0, nil, test.Equals("this was always working\r\n")),
+ },
+ {
+ Description: "--net=host -it",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("run", "--rm", "--net=host", "-it", testutil.CommonImage, "echo", "this was not working due to issue #108")
+ cmd.WithPseudoTTY()
+ return cmd
+ },
+ Expected: test.Expects(0, nil, test.Equals("this was not working due to issue #108\r\n")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/login.go b/cmd/nerdctl/login.go
deleted file mode 100644
index adb7d31b182..00000000000
--- a/cmd/nerdctl/login.go
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "errors"
- "io"
- "strings"
-
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/login"
-
- "github.com/sirupsen/logrus"
- "github.com/spf13/cobra"
-)
-
-type loginOptions struct {
- serverAddress string
- username string
- password string
- passwordStdin bool
-}
-
-var options = new(loginOptions)
-
-func newLoginCommand() *cobra.Command {
- var loginCommand = &cobra.Command{
- Use: "login [flags] [SERVER]",
- Args: cobra.MaximumNArgs(1),
- Short: "Log in to a container registry",
- RunE: loginAction,
- SilenceUsage: true,
- SilenceErrors: true,
- }
- loginCommand.Flags().StringVarP(&options.username, "username", "u", "", "Username")
- loginCommand.Flags().StringVarP(&options.password, "password", "p", "", "Password")
- loginCommand.Flags().BoolVar(&options.passwordStdin, "password-stdin", false, "Take the password from stdin")
- return loginCommand
-}
-
-func loginAction(cmd *cobra.Command, args []string) error {
- if len(args) == 1 {
- options.serverAddress = args[0]
- }
- if err := verifyLoginOptions(cmd, options); err != nil {
- return err
- }
-
- globalOptions, err := processRootCmdFlags(cmd)
- if err != nil {
- return err
- }
-
- return login.Login(cmd.Context(), types.LoginCommandOptions{
- GOptions: globalOptions,
- ServerAddress: options.serverAddress,
- Username: options.username,
- Password: options.password,
- }, cmd.OutOrStdout())
-}
-
-// copied from github.com/docker/cli/cli/command/registry/login.go (v20.10.3)
-func verifyLoginOptions(cmd *cobra.Command, options *loginOptions) error {
- if options.password != "" {
- logrus.Warn("WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
- if options.passwordStdin {
- return errors.New("--password and --password-stdin are mutually exclusive")
- }
- }
-
- if options.passwordStdin {
- if options.username == "" {
- return errors.New("must provide --username with --password-stdin")
- }
-
- contents, err := io.ReadAll(cmd.InOrStdin())
- if err != nil {
- return err
- }
-
- options.password = strings.TrimSuffix(string(contents), "\n")
- options.password = strings.TrimSuffix(options.password, "\r")
- }
- return nil
-}
diff --git a/cmd/nerdctl/login/login.go b/cmd/nerdctl/login/login.go
new file mode 100644
index 00000000000..64dc5f4b265
--- /dev/null
+++ b/cmd/nerdctl/login/login.go
@@ -0,0 +1,109 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package login
+
+import (
+ "errors"
+ "io"
+ "strings"
+
+ "github.com/spf13/cobra"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/login"
+)
+
+func NewLoginCommand() *cobra.Command {
+ var loginCommand = &cobra.Command{
+ Use: "login [flags] [SERVER]",
+ Args: cobra.MaximumNArgs(1),
+ Short: "Log in to a container registry",
+ RunE: loginAction,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+ loginCommand.Flags().StringP("username", "u", "", "Username")
+ loginCommand.Flags().StringP("password", "p", "", "Password")
+ loginCommand.Flags().Bool("password-stdin", false, "Take the password from stdin")
+ return loginCommand
+}
+
+func processLoginOptions(cmd *cobra.Command) (types.LoginCommandOptions, error) {
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.LoginCommandOptions{}, err
+ }
+
+ username, err := cmd.Flags().GetString("username")
+ if err != nil {
+ return types.LoginCommandOptions{}, err
+ }
+ password, err := cmd.Flags().GetString("password")
+ if err != nil {
+ return types.LoginCommandOptions{}, err
+ }
+ passwordStdin, err := cmd.Flags().GetBool("password-stdin")
+ if err != nil {
+ return types.LoginCommandOptions{}, err
+ }
+
+ if strings.Contains(username, ":") {
+ return types.LoginCommandOptions{}, errors.New("username cannot contain colons")
+ }
+
+ if password != "" {
+ log.L.Warn("WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
+ if passwordStdin {
+ return types.LoginCommandOptions{}, errors.New("--password and --password-stdin are mutually exclusive")
+ }
+ }
+
+ if passwordStdin {
+ if username == "" {
+ return types.LoginCommandOptions{}, errors.New("must provide --username with --password-stdin")
+ }
+
+ contents, err := io.ReadAll(cmd.InOrStdin())
+ if err != nil {
+ return types.LoginCommandOptions{}, err
+ }
+
+ password = strings.TrimSuffix(string(contents), "\n")
+ password = strings.TrimSuffix(password, "\r")
+ }
+ return types.LoginCommandOptions{
+ GOptions: globalOptions,
+ Username: username,
+ Password: password,
+ }, nil
+}
+
+func loginAction(cmd *cobra.Command, args []string) error {
+ options, err := processLoginOptions(cmd)
+ if err != nil {
+ return err
+ }
+
+ if len(args) == 1 {
+ options.ServerAddress = args[0]
+ }
+
+ return login.Login(cmd.Context(), options, cmd.OutOrStdout())
+}
diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go
new file mode 100644
index 00000000000..2ac21fa374f
--- /dev/null
+++ b/cmd/nerdctl/login/login_linux_test.go
@@ -0,0 +1,548 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// https://docs.docker.com/reference/cli/dockerd/#insecure-registries
+// Local registries, whose IP address falls in the 127.0.0.0/8 range, are automatically marked as insecure as of Docker 1.3.2.
+// It isn't recommended to rely on this, as it may change in the future.
+// "--insecure" means that either the certificates are untrusted, or that the protocol is plain http
+package login
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "strconv"
+ "testing"
+
+ "gotest.tools/v3/icmd"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testca"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry"
+)
+
+type Client struct {
+ args []string
+ configPath string
+}
+
+func (ag *Client) WithInsecure(value bool) *Client {
+ ag.args = append(ag.args, "--insecure-registry="+strconv.FormatBool(value))
+ return ag
+}
+
+func (ag *Client) WithHostsDir(hostDirs string) *Client {
+ ag.args = append(ag.args, "--hosts-dir", hostDirs)
+ return ag
+}
+
+func (ag *Client) WithCredentials(username, password string) *Client {
+ if username != "" {
+ ag.args = append(ag.args, "--username", username)
+ }
+ if password != "" {
+ ag.args = append(ag.args, "--password", password)
+ }
+ return ag
+}
+
+func (ag *Client) WithConfigPath(value string) *Client {
+ ag.configPath = value
+ return ag
+}
+
+func (ag *Client) GetConfigPath() string {
+ return ag.configPath
+}
+
+func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd {
+ if ag.configPath == "" {
+ ag.configPath, _ = os.MkdirTemp(base.T.TempDir(), "docker-config")
+ }
+ args := []string{"login"}
+ if base.Target == "nerdctl" {
+ args = append(args, "--debug-full")
+ }
+ args = append(args, ag.args...)
+ icmdCmd := icmd.Command(base.Binary, append(base.Args, append(args, host)...)...)
+ icmdCmd.Env = append(base.Env, "HOME="+os.Getenv("HOME"), "DOCKER_CONFIG="+ag.configPath)
+
+ return &testutil.Cmd{
+ Cmd: icmdCmd,
+ Base: base,
+ }
+}
+
+func TestLoginPersistence(t *testing.T) {
+ base := testutil.NewBase(t)
+ t.Parallel()
+
+ // Retrieve from the store
+ testCases := []struct {
+ auth string
+ }{
+ {
+ "basic",
+ },
+ {
+ "token",
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) {
+ t.Parallel()
+
+ username := test.RandomStringBase64(30) + "∞"
+ password := test.RandomStringBase64(30) + ":∞"
+
+ // Add the requested authentication
+ var auth testregistry.Auth
+ var dependentCleanup func(error)
+
+ auth = &testregistry.NoAuth{}
+ if tc.auth == "basic" {
+ auth = &testregistry.BasicAuth{
+ Username: username,
+ Password: password,
+ }
+ } else if tc.auth == "token" {
+ authCa := testca.New(base.T)
+ as := testregistry.NewAuthServer(base, authCa, 0, username, password, false)
+ auth = &testregistry.TokenAuth{
+ Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)),
+ CertPath: as.CertPath,
+ }
+ dependentCleanup = as.Cleanup
+ }
+
+ // Start the registry with the requested options
+ reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup)
+
+ // Register registry cleanup
+ t.Cleanup(func() {
+ reg.Cleanup(nil)
+ })
+
+ // First, login successfully
+ c := (&Client{}).
+ WithCredentials(username, password)
+
+ c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertOK()
+
+ // Now, log in successfully without passing any explicit credentials
+ nc := (&Client{}).
+ WithConfigPath(c.GetConfigPath())
+ nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertOK()
+
+ // Now fail while using invalid credentials
+ nc.WithCredentials("invalid", "invalid").
+ Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertFail()
+
+ // And login again without, reverting to the last saved good state
+ nc = (&Client{}).
+ WithConfigPath(c.GetConfigPath())
+
+ nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertOK()
+ })
+ }
+}
+
+/*
+func TestAgainstNoAuth(t *testing.T) {
+ base := testutil.NewBase(t)
+ t.Parallel()
+
+ // Start the registry with the requested options
+ reg := testregistry.NewRegistry(base, nil, 0, &testregistry.NoAuth{}, nil)
+
+ // Register registry cleanup
+ t.Cleanup(func() {
+ reg.Cleanup(nil)
+ })
+
+ c := (&Client{}).
+ WithCredentials("invalid", "invalid")
+
+ c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertOK()
+
+ content, _ := os.ReadFile(filepath.Join(c.configPath, "config.json"))
+ fmt.Println(string(content))
+
+ c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)).
+ AssertFail()
+
+}
+
+*/
+
+func TestLoginAgainstVariants(t *testing.T) {
+ // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option
+ // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication
+ testutil.DockerIncompatible(t)
+
+ base := testutil.NewBase(t)
+ t.Parallel()
+
+ testCases := []struct {
+ port int
+ tls bool
+ auth string
+ }{
+ // Basic auth, no TLS
+ {
+ 80,
+ false,
+ "basic",
+ },
+ {
+ 443,
+ false,
+ "basic",
+ },
+ {
+ 0,
+ false,
+ "basic",
+ },
+ // Token auth, no TLS
+ {
+ 80,
+ false,
+ "token",
+ },
+ {
+ 443,
+ false,
+ "token",
+ },
+ {
+ 0,
+ false,
+ "token",
+ },
+ // Basic auth, with TLS
+ /*
+ // This is not working currently, unless we would force a server https:// in hosts
+ // To be fixed with login rewrite
+ {
+ 80,
+ true,
+ "basic",
+ },
+ */
+ {
+ 443,
+ true,
+ "basic",
+ },
+ {
+ 0,
+ true,
+ "basic",
+ },
+ // Token auth, with TLS
+ /*
+ // This is not working currently, unless we would force a server https:// in hosts
+ // To be fixed with login rewrite
+ {
+ 80,
+ true,
+ "token",
+ },
+ */
+ {
+ 443,
+ true,
+ "token",
+ },
+ {
+ 0,
+ true,
+ "token",
+ },
+ }
+
+ // Iterate through all cases, that will present a variety of port (80, 443, random), TLS (yes or no), and authentication (basic, token) type combinations
+ for _, tc := range testCases {
+ port := tc.port
+ tls := tc.tls
+ auth := tc.auth
+
+ t.Run(fmt.Sprintf("Login against `tls: %t port: %d auth: %s`", tls, port, auth), func(t *testing.T) {
+ // Tests with fixed ports should not be parallelized (although the port locking mechanism will prevent conflicts)
+ // as their children tests are parallelized, and this might deadlock given the way `Parallel` works
+ if port == 0 {
+ t.Parallel()
+ }
+
+ // Generate credentials that are specific to each registry, so that we never cross hit another one
+ username := test.RandomStringBase64(30) + "∞"
+ password := test.RandomStringBase64(30) + ":∞"
+
+ // Get a CA if we want TLS
+ var ca *testca.CA
+ if tls {
+ ca = testca.New(base.T)
+ }
+
+ // Add the requested authenticator
+ var authenticator testregistry.Auth
+ var dependentCleanup func(error)
+
+ authenticator = &testregistry.NoAuth{}
+ if auth == "basic" {
+ authenticator = &testregistry.BasicAuth{
+ Username: username,
+ Password: password,
+ }
+ } else if auth == "token" {
+ authCa := ca
+ // We could be on !tls, meaning no ca - but we still need a CA to sign jwt tokens
+ if authCa == nil {
+ authCa = testca.New(base.T)
+ }
+ as := testregistry.NewAuthServer(base, authCa, 0, username, password, tls)
+ authenticator = &testregistry.TokenAuth{
+ Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)),
+ CertPath: as.CertPath,
+ }
+ dependentCleanup = as.Cleanup
+ }
+
+ // Start the registry with the requested options
+ reg := testregistry.NewRegistry(base, ca, port, authenticator, dependentCleanup)
+
+ // Register registry cleanup
+ t.Cleanup(func() {
+ reg.Cleanup(nil)
+ })
+
+ // Any registry is reachable through its ip+port, and localhost variants
+ regHosts := []string{
+ net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)),
+ net.JoinHostPort("localhost", strconv.Itoa(reg.Port)),
+ net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)),
+ // TODO: ipv6
+ // net.JoinHostPort("::1", strconv.Itoa(reg.Port)),
+ }
+
+ // Registries that use port 443 also allow access without specifying a port
+ if reg.Port == 443 {
+ regHosts = append(regHosts, reg.IP.String())
+ regHosts = append(regHosts, "localhost")
+ regHosts = append(regHosts, "127.0.0.1")
+ // TODO: ipv6
+ // regHosts = append(regHosts, "::1")
+ }
+
+ // Iterate through these hosts access points, and create a test per-variant
+ for _, value := range regHosts {
+ regHost := value
+ t.Run(regHost, func(t *testing.T) {
+ t.Parallel()
+
+ // 1. test with valid credentials but no access to the CA
+ t.Run("1. valid credentials (no CA) ", func(t *testing.T) {
+ t.Parallel()
+
+ c := (&Client{}).
+ WithCredentials(username, password)
+
+ rl, _ := dockerconfigresolver.Parse(regHost)
+ // a. Insecure flag not being set
+ // TODO: remove specialization when we fix the localhost mess
+ if rl.IsLocalhost() && !tls {
+ c.Run(base, regHost).
+ AssertOK()
+ } else {
+ c.Run(base, regHost).
+ AssertFail()
+ }
+
+ // b. Insecure flag set to false
+ // TODO: remove specialization when we fix the localhost mess
+ if !rl.IsLocalhost() {
+ (&Client{}).
+ WithCredentials(username, password).
+ WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+ }
+
+ // c. Insecure flag set to true
+ // TODO: remove specialization when we fix the localhost mess
+ if !rl.IsLocalhost() || !tls {
+ (&Client{}).
+ WithCredentials(username, password).
+ WithInsecure(true).
+ Run(base, regHost).
+ AssertOK()
+ }
+ })
+
+ // 2. test with valid credentials AND access to the CA
+ t.Run("2. valid credentials (with access to server CA)", func(t *testing.T) {
+ t.Parallel()
+
+ rl, _ := dockerconfigresolver.Parse(regHost)
+
+ // a. Insecure flag not being set
+ c := (&Client{}).
+ WithCredentials(username, password).
+ WithHostsDir(reg.HostsDir)
+
+ if tls || rl.IsLocalhost() {
+ c.Run(base, regHost).
+ AssertOK()
+ } else {
+ c.Run(base, regHost).
+ AssertFail()
+ }
+
+ // b. Insecure flag set to false
+ if tls {
+ c.WithInsecure(false).
+ Run(base, regHost).
+ AssertOK()
+ } else {
+ // TODO: remove specialization when we fix the localhost mess
+ if !rl.IsLocalhost() {
+ c.WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+ }
+ }
+
+ // c. Insecure flag set to true
+ c.WithInsecure(true).
+ Run(base, regHost).
+ AssertOK()
+ })
+
+ t.Run("3. valid credentials, any url variant, should always succeed", func(t *testing.T) {
+ t.Parallel()
+ c := (&Client{}).
+ WithCredentials(username, password).
+ WithHostsDir(reg.HostsDir).
+ // Just use insecure here for all servers - it does not matter for what we are testing here
+ // in this case, which is whether we can successfully log in against any of these variants
+ WithInsecure(true)
+
+ // TODO: remove specialization when we fix the localhost mess
+ rl, _ := dockerconfigresolver.Parse(regHost)
+ if !rl.IsLocalhost() || !tls {
+ c.Run(base, "http://"+regHost).AssertOK()
+ c.Run(base, "https://"+regHost).AssertOK()
+ c.Run(base, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").AssertOK()
+ c.Run(base, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").AssertOK()
+ }
+ })
+
+ t.Run("4. wrong password should always fail", func(t *testing.T) {
+ t.Parallel()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ WithHostsDir(reg.HostsDir).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ WithHostsDir(reg.HostsDir).
+ WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ WithHostsDir(reg.HostsDir).
+ WithInsecure(true).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials(username, "invalid").
+ WithInsecure(true).
+ Run(base, regHost).
+ AssertFail()
+ })
+
+ t.Run("5. wrong username should always fail", func(t *testing.T) {
+ t.Parallel()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ WithHostsDir(reg.HostsDir).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ WithHostsDir(reg.HostsDir).
+ WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ WithHostsDir(reg.HostsDir).
+ WithInsecure(true).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ WithInsecure(false).
+ Run(base, regHost).
+ AssertFail()
+
+ (&Client{}).
+ WithCredentials("invalid", password).
+ WithInsecure(true).
+ Run(base, regHost).
+ AssertFail()
+ })
+ })
+ }
+ })
+ }
+}
diff --git a/cmd/nerdctl/login/login_test.go b/cmd/nerdctl/login/login_test.go
new file mode 100644
index 00000000000..ca5ad784c9b
--- /dev/null
+++ b/cmd/nerdctl/login/login_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package login
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/login/logout.go b/cmd/nerdctl/login/logout.go
new file mode 100644
index 00000000000..f43643b791f
--- /dev/null
+++ b/cmd/nerdctl/login/logout.go
@@ -0,0 +1,66 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package login
+
+import (
+ "github.com/spf13/cobra"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/cmd/logout"
+)
+
+func NewLogoutCommand() *cobra.Command {
+ return &cobra.Command{
+ Use: "logout [flags] [SERVER]",
+ Args: cobra.MaximumNArgs(1),
+ Short: "Log out from a container registry",
+ RunE: logoutAction,
+ ValidArgsFunction: logoutShellComplete,
+ SilenceUsage: true,
+ SilenceErrors: true,
+ }
+}
+
+func logoutAction(cmd *cobra.Command, args []string) error {
+ logoutServer := ""
+ if len(args) > 0 {
+ logoutServer = args[0]
+ }
+
+ errGroup, err := logout.Logout(cmd.Context(), logoutServer)
+ if err != nil {
+ log.L.WithError(err).Errorf("Failed to erase credentials for: %s", logoutServer)
+ }
+ if errGroup != nil {
+ log.L.Error("None of the following entries could be found")
+ for _, v := range errGroup {
+ log.L.Errorf("%s", v)
+ }
+ }
+
+ return err
+}
+
+func logoutShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+ candidates, err := logout.ShellCompletion()
+ if err != nil {
+ return nil, cobra.ShellCompDirectiveError
+ }
+
+ return candidates, cobra.ShellCompDirectiveNoFileComp
+}
diff --git a/cmd/nerdctl/login_linux_test.go b/cmd/nerdctl/login_linux_test.go
deleted file mode 100644
index 12fbc8d4377..00000000000
--- a/cmd/nerdctl/login_linux_test.go
+++ /dev/null
@@ -1,191 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "net"
- "path"
- "strconv"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/containerd/nerdctl/pkg/testutil/testregistry"
-)
-
-func TestLogin(t *testing.T) {
- // Skip docker, because Docker doesn't have `--hosts-dir` option, and we don't want to contaminate the global /etc/docker/certs.d during this test
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewHTTPS(base, "admin", "validTestPassword")
- defer reg.Cleanup()
-
- regHost := net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.ListenPort))
-
- t.Logf("Good password")
- base.Cmd("--debug-full", "--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "validTestPassword", regHost).AssertOK()
-
- t.Logf("Bad password")
- base.Cmd("--debug-full", "--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "invalidTestPassword", regHost).AssertFail()
-}
-
-func TestLoginWithSpecificRegHosts(t *testing.T) {
- // Skip docker, because Docker doesn't have `--hosts-dir` option, and we don't want to contaminate the global /etc/docker/certs.d during this test
- testutil.DockerIncompatible(t)
-
- base := testutil.NewBase(t)
- reg := testregistry.NewHTTPS(base, "admin", "validTestPassword")
- defer reg.Cleanup()
-
- regHost := net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.ListenPort))
-
- t.Logf("Prepare regHost URL with path and Scheme")
-
- type testCase struct {
- url string
- log string
- }
- testCases := []testCase{
- {
- url: "https://" + path.Join(regHost, "test"),
- log: "Login with repository containing path and scheme in the URL",
- },
- {
- url: path.Join(regHost, "test"),
- log: "Login with repository containing path and without scheme in the URL",
- },
- }
- for _, tc := range testCases {
- t.Logf(tc.log)
- base.Cmd("--debug-full", "--hosts-dir", reg.HostsDir, "login", "-u", "admin", "-p", "validTestPassword", tc.url).AssertOK()
- }
-
-}
-
-func TestLoginWithPlainHttp(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- reg5000 := testregistry.NewAuthWithHTTP(base, "admin", "validTestPassword", 5000, 5001)
- reg80 := testregistry.NewAuthWithHTTP(base, "admin", "validTestPassword", 80, 5002)
- defer reg5000.Cleanup()
- defer reg80.Cleanup()
- testCasesForPort5000 := []struct {
- regHost string
- regPort int
- useRegPort bool
- username string
- password string
- shouldSuccess bool
- registry *testregistry.TestRegistry
- shouldUseInSecure bool
- }{
- {
- regHost: "127.0.0.1",
- regPort: 5000,
- useRegPort: true,
- username: "admin",
- password: "validTestPassword",
- shouldSuccess: true,
- registry: reg5000,
- shouldUseInSecure: true,
- },
- {
- regHost: "127.0.0.1",
- regPort: 5000,
- useRegPort: true,
- username: "admin",
- password: "invalidTestPassword",
- shouldSuccess: false,
- registry: reg5000,
- shouldUseInSecure: true,
- },
- {
- regHost: "127.0.0.1",
- regPort: 5000,
- useRegPort: true,
- username: "admin",
- password: "validTestPassword",
- // Following the merging of the below, any localhost/loopback registries will
- // get automatically downgraded to HTTP so this will still succceed:
- // https://github.com/containerd/containerd/pull/7393
- shouldSuccess: true,
- registry: reg5000,
- shouldUseInSecure: false,
- },
- {
- regHost: "127.0.0.1",
- regPort: 80,
- useRegPort: false,
- username: "admin",
- password: "validTestPassword",
- shouldSuccess: true,
- registry: reg80,
- shouldUseInSecure: true,
- },
- {
- regHost: "127.0.0.1",
- regPort: 80,
- useRegPort: false,
- username: "admin",
- password: "invalidTestPassword",
- shouldSuccess: false,
- registry: reg80,
- shouldUseInSecure: true,
- },
- {
- regHost: "127.0.0.1",
- regPort: 80,
- useRegPort: false,
- username: "admin",
- password: "validTestPassword",
- // Following the merging of the below, any localhost/loopback registries will
- // get automatically downgraded to HTTP so this will still succceed:
- // https://github.com/containerd/containerd/pull/7393
- shouldSuccess: true,
- registry: reg80,
- shouldUseInSecure: false,
- },
- }
- for _, tc := range testCasesForPort5000 {
- tcName := fmt.Sprintf("%+v", tc)
- t.Run(tcName, func(t *testing.T) {
- regHost := tc.regHost
- if tc.useRegPort {
- regHost = fmt.Sprintf("%s:%d", regHost, tc.regPort)
- }
- if tc.shouldSuccess {
- t.Logf("Good password")
- } else {
- t.Logf("Bad password")
- }
- var args []string
- if tc.shouldUseInSecure {
- args = append(args, "--insecure-registry")
- }
- args = append(args, []string{
- "--debug-full", "--hosts-dir", tc.registry.HostsDir, "login", "-u", tc.username, "-p", tc.password, regHost,
- }...)
- cmd := base.Cmd(args...)
- if tc.shouldSuccess {
- cmd.AssertOK()
- } else {
- cmd.AssertFail()
- }
- })
- }
-}
diff --git a/cmd/nerdctl/logout.go b/cmd/nerdctl/logout.go
deleted file mode 100644
index 09b4ee43179..00000000000
--- a/cmd/nerdctl/logout.go
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
-
- "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
- dockercliconfig "github.com/docker/cli/cli/config"
- "github.com/spf13/cobra"
-)
-
-func newLogoutCommand() *cobra.Command {
- var logoutCommand = &cobra.Command{
- Use: "logout [flags] [SERVER]",
- Args: cobra.MaximumNArgs(1),
- Short: "Log out from a container registry",
- RunE: logoutAction,
- ValidArgsFunction: logoutShellComplete,
- SilenceUsage: true,
- SilenceErrors: true,
- }
- return logoutCommand
-}
-
-// code inspired from XXX
-func logoutAction(cmd *cobra.Command, args []string) error {
- serverAddress := dockerconfigresolver.IndexServer
- isDefaultRegistry := true
- if len(args) >= 1 {
- serverAddress = args[0]
- isDefaultRegistry = false
- }
-
- var (
- regsToLogout = []string{serverAddress}
- hostnameAddress = serverAddress
- )
-
- if !isDefaultRegistry {
- hostnameAddress = dockerconfigresolver.ConvertToHostname(serverAddress)
- // the tries below are kept for backward compatibility where a user could have
- // saved the registry in one of the following format.
- regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress)
- }
-
- fmt.Fprintf(cmd.OutOrStdout(), "Removing login credentials for %s\n", hostnameAddress)
-
- dockerConfigFile, err := dockercliconfig.Load("")
- if err != nil {
- return err
- }
- errs := make(map[string]error)
- for _, r := range regsToLogout {
- if err := dockerConfigFile.GetCredentialsStore(r).Erase(r); err != nil {
- errs[r] = err
- }
- }
-
- // if at least one removal succeeded, report success. Otherwise report errors
- if len(errs) == len(regsToLogout) {
- fmt.Fprintln(cmd.ErrOrStderr(), "WARNING: could not erase credentials:")
- for k, v := range errs {
- fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", k, v)
- }
- }
-
- return nil
-}
-
-func logoutShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- dockerConfigFile, err := dockercliconfig.Load("")
- if err != nil {
- return nil, cobra.ShellCompDirectiveError
- }
- candidates := []string{}
- for key := range dockerConfigFile.AuthConfigs {
- candidates = append(candidates, key)
- }
- return candidates, cobra.ShellCompDirectiveNoFileComp
-}
diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go
index 6113790c8e0..36ef2fc079f 100644
--- a/cmd/nerdctl/main.go
+++ b/cmd/nerdctl/main.go
@@ -21,27 +21,36 @@ import (
"fmt"
"os"
"runtime"
- "strconv"
"strings"
- "time"
-
- "github.com/containerd/nerdctl/pkg/config"
- ncdefaults "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/logging"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/version"
- "github.com/fatih/color"
- "github.com/pelletier/go-toml"
- "github.com/sirupsen/logrus"
+ "github.com/fatih/color"
+ "github.com/pelletier/go-toml/v2"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
-)
-const (
- Category = "category"
- Management = "management"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/compose"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/container"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/image"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/inspect"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/internal"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/ipfs"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/login"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/system"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/volume"
+ "github.com/containerd/nerdctl/v2/pkg/config"
+ ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/logging"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/store"
+ "github.com/containerd/nerdctl/v2/pkg/version"
)
var (
@@ -72,7 +81,7 @@ func usage(c *cobra.Command) error {
if f.Hidden {
continue
}
- if f.Annotations[Category] == Management {
+ if f.Annotations[helpers.Category] == helpers.Management {
managementCommands = append(managementCommands, f)
} else {
nonManagementCommands = append(nonManagementCommands, f)
@@ -100,7 +109,7 @@ func usage(c *cobra.Command) error {
t += "\n"
return t
}
- s += printCommands("Management commands", managementCommands)
+ s += printCommands("helpers.Management commands", managementCommands)
s += printCommands("Commands", nonManagementCommands)
s += Bold("Flags") + ":\n"
@@ -118,7 +127,7 @@ func usage(c *cobra.Command) error {
func main() {
if err := xmain(); err != nil {
errutil.HandleExitCoder(err)
- logrus.Fatal(err)
+ log.L.Fatal(err)
}
}
@@ -139,15 +148,15 @@ func xmain() error {
func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, error) {
cfg := config.New()
if r, err := os.Open(tomlPath); err == nil {
- logrus.Debugf("Loading config from %q", tomlPath)
+ log.L.Debugf("Loading config from %q", tomlPath)
defer r.Close()
- dec := toml.NewDecoder(r).Strict(true) // set Strict to detect typo
+ dec := toml.NewDecoder(r).DisallowUnknownFields() // set Strict to detect typo
if err := dec.Decode(cfg); err != nil {
return nil, fmt.Errorf("failed to load nerdctl config (not daemon config) from %q (Hint: don't mix up daemon's `config.toml` with `nerdctl.toml`): %w", tomlPath, err)
}
- logrus.Debugf("Loaded config %+v", cfg)
+ log.L.Debugf("Loaded config %+v", cfg)
} else {
- logrus.WithError(err).Debugf("Not loading config from %q", tomlPath)
+ log.L.WithError(err).Debugf("Not loading config from %q", tomlPath)
if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
@@ -157,24 +166,26 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet,
rootCmd.PersistentFlags().Bool("debug", cfg.Debug, "debug mode")
rootCmd.PersistentFlags().Bool("debug-full", cfg.DebugFull, "debug mode (with full output)")
// -a is aliases (conflicts with nerdctl images -a)
- AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`)
+ helpers.AddPersistentStringFlag(rootCmd, "address", []string{"a", "H"}, nil, []string{"host"}, aliasToBeInherited, cfg.Address, "CONTAINERD_ADDRESS", `containerd address, optionally with "unix://" prefix`)
// -n is aliases (conflicts with nerdctl logs -n)
- AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`)
- rootCmd.RegisterFlagCompletionFunc("namespace", shellCompleteNamespaceNames)
- AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter")
- rootCmd.RegisterFlagCompletionFunc("snapshotter", shellCompleteSnapshotterNames)
- rootCmd.RegisterFlagCompletionFunc("storage-driver", shellCompleteSnapshotterNames)
- AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory")
- AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory")
+ helpers.AddPersistentStringFlag(rootCmd, "namespace", []string{"n"}, nil, nil, aliasToBeInherited, cfg.Namespace, "CONTAINERD_NAMESPACE", `containerd namespace, such as "moby" for Docker, "k8s.io" for Kubernetes`)
+ rootCmd.RegisterFlagCompletionFunc("namespace", completion.NamespaceNames)
+ helpers.AddPersistentStringFlag(rootCmd, "snapshotter", nil, nil, []string{"storage-driver"}, aliasToBeInherited, cfg.Snapshotter, "CONTAINERD_SNAPSHOTTER", "containerd snapshotter")
+ rootCmd.RegisterFlagCompletionFunc("snapshotter", completion.SnapshotterNames)
+ rootCmd.RegisterFlagCompletionFunc("storage-driver", completion.SnapshotterNames)
+ helpers.AddPersistentStringFlag(rootCmd, "cni-path", nil, nil, nil, aliasToBeInherited, cfg.CNIPath, "CNI_PATH", "cni plugins binary directory")
+ helpers.AddPersistentStringFlag(rootCmd, "cni-netconfpath", nil, nil, nil, aliasToBeInherited, cfg.CNINetConfPath, "NETCONFPATH", "cni config directory")
rootCmd.PersistentFlags().String("data-root", cfg.DataRoot, "Root directory of persistent nerdctl state (managed by nerdctl, not by containerd)")
rootCmd.PersistentFlags().String("cgroup-manager", cfg.CgroupManager, `Cgroup manager to use ("cgroupfs"|"systemd")`)
- rootCmd.RegisterFlagCompletionFunc("cgroup-manager", shellCompleteCgroupManagerNames)
+ rootCmd.RegisterFlagCompletionFunc("cgroup-manager", completion.CgroupManagerNames)
rootCmd.PersistentFlags().Bool("insecure-registry", cfg.InsecureRegistry, "skips verifying HTTPS certs, and allows falling back to plain HTTP")
// hosts-dir is defined as StringSlice, not StringArray, to allow specifying "--hosts-dir=/etc/containerd/certs.d,/etc/docker/certs.d"
rootCmd.PersistentFlags().StringSlice("hosts-dir", cfg.HostsDir, "A directory that contains /hosts.toml (containerd style) or /{ca.cert, cert.pem, key.pem} (docker style)")
// Experimental enable experimental feature, see in https://github.com/containerd/nerdctl/blob/main/docs/experimental.md
- AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md")
- AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host")
+ helpers.AddPersistentBoolFlag(rootCmd, "experimental", nil, nil, cfg.Experimental, "NERDCTL_EXPERIMENTAL", "Control experimental: https://github.com/containerd/nerdctl/blob/main/docs/experimental.md")
+ helpers.AddPersistentStringFlag(rootCmd, "host-gateway-ip", nil, nil, nil, aliasToBeInherited, cfg.HostGatewayIP, "NERDCTL_HOST_GATEWAY_IP", "IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host")
+ helpers.AddPersistentStringFlag(rootCmd, "bridge-ip", nil, nil, nil, aliasToBeInherited, cfg.BridgeIP, "NERDCTL_BRIDGE_IP", "IP address for the default nerdctl bridge network")
+ rootCmd.PersistentFlags().Bool("kube-hide-dupe", cfg.KubeHideDupe, "Deduplicate images for Kubernetes with namespace k8s.io")
return aliasToBeInherited, nil
}
@@ -194,7 +205,7 @@ Config file ($NERDCTL_TOML): %s
Use: "nerdctl",
Short: short,
Long: long,
- Version: strings.TrimPrefix(version.Version, "v"),
+ Version: strings.TrimPrefix(version.GetVersion(), "v"),
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true, // required for global short hands like -a, -H, -n
@@ -207,7 +218,7 @@ Config file ($NERDCTL_TOML): %s
}
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -216,7 +227,7 @@ Config file ($NERDCTL_TOML): %s
debug = globalOptions.Debug
}
if debug {
- logrus.SetLevel(logrus.DebugLevel)
+ log.SetLevel(log.DebugLevel.String())
}
address := globalOptions.Address
if strings.Contains(address, "://") && !strings.HasPrefix(address, "unix://") {
@@ -230,91 +241,103 @@ Config file ($NERDCTL_TOML): %s
return fmt.Errorf("invalid cgroup-manager %q (supported values: \"systemd\", \"cgroupfs\", \"none\")", cgroupManager)
}
}
+
+ // Since we store containers' stateful information on the filesystem per namespace, we need namespaces to be
+ // valid, safe path segments. This is enforced by store.ValidatePathComponent.
+ // Note that the container runtime will further enforce additional restrictions on namespace names
+ // (containerd treats namespaces as valid identifiers - eg: alphanumericals + dash, starting with a letter)
+ // See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#path-segment-names for
+ // considerations about path segments identifiers.
+ if err = store.ValidatePathComponent(globalOptions.Namespace); err != nil {
+ return err
+ }
if appNeedsRootlessParentMain(cmd, args) {
// reexec /proc/self/exe with `nsenter` into RootlessKit namespaces
return rootlessutil.ParentMain(globalOptions.HostGatewayIP)
}
return nil
}
- rootCmd.RunE = unknownSubcommandAction
+ rootCmd.RunE = helpers.UnknownSubcommandAction
rootCmd.AddCommand(
- newCreateCommand(),
+ container.NewCreateCommand(),
// #region Run & Exec
- newRunCommand(),
- newUpdateCommand(),
- newExecCommand(),
+ container.NewRunCommand(),
+ container.NewUpdateCommand(),
+ container.NewExecCommand(),
// #endregion
// #region Container management
- newPsCommand(),
- newLogsCommand(),
- newPortCommand(),
- newStopCommand(),
- newStartCommand(),
- newRestartCommand(),
- newKillCommand(),
- newRmCommand(),
- newPauseCommand(),
- newUnpauseCommand(),
- newCommitCommand(),
- newWaitCommand(),
- newRenameCommand(),
+ container.NewPsCommand(),
+ container.NewLogsCommand(),
+ container.NewPortCommand(),
+ container.NewStopCommand(),
+ container.NewStartCommand(),
+ container.NewDiffCommand(),
+ container.NewRestartCommand(),
+ container.NewKillCommand(),
+ container.NewRmCommand(),
+ container.NewPauseCommand(),
+ container.NewUnpauseCommand(),
+ container.NewCommitCommand(),
+ container.NewWaitCommand(),
+ container.NewRenameCommand(),
+ container.NewAttachCommand(),
// #endregion
// Build
- newBuildCommand(),
+ builder.NewBuildCommand(),
// #region Image management
- newImagesCommand(),
- newPullCommand(),
- newPushCommand(),
- newLoadCommand(),
- newSaveCommand(),
- newTagCommand(),
- newRmiCommand(),
- newHistoryCommand(),
+ image.NewImagesCommand(),
+ image.NewPullCommand(),
+ image.NewPushCommand(),
+ image.NewLoadCommand(),
+ image.NewSaveCommand(),
+ image.NewTagCommand(),
+ image.NewRmiCommand(),
+ image.NewHistoryCommand(),
// #endregion
// #region System
- newEventsCommand(),
- newInfoCommand(),
+ system.NewEventsCommand(),
+ system.NewInfoCommand(),
newVersionCommand(),
// #endregion
// Inspect
- newInspectCommand(),
+ inspect.NewInspectCommand(),
// stats
- newTopCommand(),
- newStatsCommand(),
-
- // #region Management
- newContainerCommand(),
- newImageCommand(),
- newNetworkCommand(),
- newVolumeCommand(),
- newSystemCommand(),
- newNamespaceCommand(),
- newBuilderCommand(),
+ container.NewTopCommand(),
+ container.NewStatsCommand(),
+
+ // #region helpers.Management
+ container.NewContainerCommand(),
+ image.NewImageCommand(),
+ network.NewNetworkCommand(),
+ volume.NewVolumeCommand(),
+ system.NewSystemCommand(),
+ namespace.NewNamespaceCommand(),
+ builder.NewBuilderCommand(),
// #endregion
// Internal
- newInternalCommand(),
+ internal.NewInternalCommand(),
// login
- newLoginCommand(),
+ login.NewLoginCommand(),
// Logout
- newLogoutCommand(),
+ login.NewLogoutCommand(),
// Compose
- newComposeCommand(),
+ compose.NewComposeCommand(),
// IPFS
- newIPFSCommand(),
+ ipfs.NewIPFSCommand(),
)
addApparmorCommand(rootCmd)
- addCpCommand(rootCmd)
+ container.AddCpCommand(rootCmd)
// add aliasToBeInherited to subCommand(s) InheritedFlags
for _, subCmd := range rootCmd.Commands() {
@@ -322,271 +345,3 @@ Config file ($NERDCTL_TOML): %s
}
return rootCmd, nil
}
-
-func globalFlags(cmd *cobra.Command) (string, []string) {
- args0, err := os.Executable()
- if err != nil {
- logrus.WithError(err).Warnf("cannot call os.Executable(), assuming the executable to be %q", os.Args[0])
- args0 = os.Args[0]
- }
- if len(os.Args) < 2 {
- return args0, nil
- }
-
- rootCmd := cmd.Root()
- flagSet := rootCmd.Flags()
- args := []string{}
- flagSet.VisitAll(func(f *pflag.Flag) {
- key := f.Name
- val := f.Value.String()
- if f.Changed {
- args = append(args, "--"+key+"="+val)
- }
- })
- return args0, args
-}
-
-// unknownSubcommandAction is needed to let `nerdctl system non-existent-command` fail
-// https://github.com/containerd/nerdctl/issues/487
-//
-// Ideally this should be implemented in Cobra itself.
-func unknownSubcommandAction(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return cmd.Help()
- }
- // The output mimics https://github.com/spf13/cobra/blob/v1.2.1/command.go#L647-L662
- msg := fmt.Sprintf("unknown subcommand %q for %q", args[0], cmd.Name())
- if suggestions := cmd.SuggestionsFor(args[0]); len(suggestions) > 0 {
- msg += "\n\nDid you mean this?\n"
- for _, s := range suggestions {
- msg += fmt.Sprintf("\t%v\n", s)
- }
- }
- return errors.New(msg)
-}
-
-// AddStringFlag is similar to cmd.Flags().String but supports aliases and env var
-func AddStringFlag(cmd *cobra.Command, name string, aliases []string, value string, env, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- value = envV
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new(string)
- flags := cmd.Flags()
- flags.StringVar(p, name, value, usage)
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.StringVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.StringVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-// AddIntFlag is similar to cmd.Flags().Int but supports aliases and env var
-func AddIntFlag(cmd *cobra.Command, name string, aliases []string, value int, env, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- v, err := strconv.ParseInt(envV, 10, 64)
- if err != nil {
- logrus.WithError(err).Warnf("Invalid int value for `%s`", env)
- }
- value = int(v)
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new(int)
- flags := cmd.Flags()
- flags.IntVar(p, name, value, usage)
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.IntVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.IntVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-// AddDurationFlag is similar to cmd.Flags().Duration but supports aliases and env var
-func AddDurationFlag(cmd *cobra.Command, name string, aliases []string, value time.Duration, env, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- var err error
- value, err = time.ParseDuration(envV)
- if err != nil {
- logrus.WithError(err).Warnf("Invalid duration value for `%s`", env)
- }
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new(time.Duration)
- flags := cmd.Flags()
- flags.DurationVar(p, name, value, usage)
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.DurationVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.DurationVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-// AddPersistentStringFlag is similar to AddStringFlag but persistent.
-// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
-func AddPersistentStringFlag(cmd *cobra.Command, name string, aliases, localAliases, persistentAliases []string, aliasToBeInherited *pflag.FlagSet, value string, env, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- value = envV
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new(string)
-
- // flags is full set of flag(s)
- // flags can redefine alias already used in subcommands
- flags := cmd.Flags()
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.StringVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.StringVar(p, a, value, aliasesUsage)
- }
- // non-persistent flags are not added to the InheritedFlags, so we should add them manually
- f := flags.Lookup(a)
- aliasToBeInherited.AddFlag(f)
- }
-
- // localFlags are local to the rootCmd
- localFlags := cmd.LocalFlags()
- for _, a := range localAliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- localFlags.StringVarP(p, a, a, value, aliasesUsage)
- } else {
- localFlags.StringVar(p, a, value, aliasesUsage)
- }
- }
-
- // persistentFlags cannot redefine alias already used in subcommands
- persistentFlags := cmd.PersistentFlags()
- persistentFlags.StringVar(p, name, value, usage)
- for _, a := range persistentAliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- persistentFlags.StringVarP(p, a, a, value, aliasesUsage)
- } else {
- persistentFlags.StringVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-// AddPersistentBoolFlag is similar to AddBoolFlag but persistent.
-// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
-func AddPersistentBoolFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value bool, env, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- var err error
- value, err = strconv.ParseBool(envV)
- if err != nil {
- logrus.WithError(err).Warnf("Invalid boolean value for `%s`", env)
- }
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new(bool)
- flags := cmd.Flags()
- for _, a := range nonPersistentAliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.BoolVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.BoolVar(p, a, value, aliasesUsage)
- }
- }
-
- persistentFlags := cmd.PersistentFlags()
- persistentFlags.BoolVar(p, name, value, usage)
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- persistentFlags.BoolVarP(p, a, a, value, aliasesUsage)
- } else {
- persistentFlags.BoolVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-// AddPersistentStringArrayFlag is similar to cmd.Flags().StringArray but supports aliases and env var and persistent.
-// See https://github.com/spf13/cobra/blob/main/user_guide.md#persistent-flags to learn what is "persistent".
-func AddPersistentStringArrayFlag(cmd *cobra.Command, name string, aliases, nonPersistentAliases []string, value []string, env string, usage string) {
- if env != "" {
- usage = fmt.Sprintf("%s [$%s]", usage, env)
- }
- if envV, ok := os.LookupEnv(env); ok {
- value = []string{envV}
- }
- aliasesUsage := fmt.Sprintf("Alias of --%s", name)
- p := new([]string)
- flags := cmd.Flags()
- for _, a := range nonPersistentAliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- flags.StringArrayVarP(p, a, a, value, aliasesUsage)
- } else {
- flags.StringArrayVar(p, a, value, aliasesUsage)
- }
- }
-
- persistentFlags := cmd.PersistentFlags()
- persistentFlags.StringArrayVar(p, name, value, usage)
- for _, a := range aliases {
- if len(a) == 1 {
- // pflag doesn't support short-only flags, so we have to register long one as well here
- persistentFlags.StringArrayVarP(p, a, a, value, aliasesUsage)
- } else {
- persistentFlags.StringArrayVar(p, a, value, aliasesUsage)
- }
- }
-}
-
-func checkExperimental(feature string) func(cmd *cobra.Command, args []string) error {
- return func(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
- if err != nil {
- return err
- }
- if !globalOptions.Experimental {
- return fmt.Errorf("%s is experimental feature, you should enable experimental config", feature)
- }
- return nil
- }
-}
-
-// IsExactArgs returns an error if there is not the exact number of args
-func IsExactArgs(number int) cobra.PositionalArgs {
- return func(cmd *cobra.Command, args []string) error {
- if len(args) == number {
- return nil
- }
- return fmt.Errorf(
- "%q requires exactly %d %s.\nSee '%s --help'.\n\nUsage: %s\n\n%s",
- cmd.CommandPath(),
- number,
- "argument(s)",
- cmd.CommandPath(),
- cmd.UseLine(),
- cmd.Short,
- )
- }
-}
diff --git a/cmd/nerdctl/main_freebsd.go b/cmd/nerdctl/main_freebsd.go
index 30ed7a9c1ea..391d34cfeed 100644
--- a/cmd/nerdctl/main_freebsd.go
+++ b/cmd/nerdctl/main_freebsd.go
@@ -24,14 +24,6 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool {
return false
}
-func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
-
func addApparmorCommand(rootCmd *cobra.Command) {
// NOP
}
-
-func addCpCommand(rootCmd *cobra.Command) {
- // NOP
-}
diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go
index a20cb00dbb6..44694688c61 100644
--- a/cmd/nerdctl/main_linux.go
+++ b/cmd/nerdctl/main_linux.go
@@ -17,10 +17,11 @@
package main
import (
- ncdefaults "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/apparmor"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool {
@@ -37,10 +38,10 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool {
return true
}
switch commands[1] {
- // completion, login, logout: false, because it shouldn't require the daemon to be running
+ // completion, login, logout, version: false, because it shouldn't require the daemon to be running
// apparmor: false, because it requires the initial mount namespace to access /sys/kernel/security
- // cp: false, because it requires the initial mount namespace to inspect file owners
- case "", "completion", "login", "logout", "apparmor", "cp":
+ // cp, compose cp: false, because it requires the initial mount namespace to inspect file owners
+ case "", "completion", "login", "logout", "apparmor", "cp", "version":
return false
case "container":
if len(commands) < 3 {
@@ -50,25 +51,18 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool {
case "cp":
return false
}
+ case "compose":
+ if len(commands) < 3 {
+ return true
+ }
+ switch commands[2] {
+ case "cp":
+ return false
+ }
}
return true
}
-func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- candidates := []string{"cgroupfs"}
- if ncdefaults.IsSystemdAvailable() {
- candidates = append(candidates, "systemd")
- }
- if rootlessutil.IsRootless() {
- candidates = append(candidates, "none")
- }
- return candidates, cobra.ShellCompDirectiveNoFileComp
-}
-
func addApparmorCommand(rootCmd *cobra.Command) {
- rootCmd.AddCommand(newApparmorCommand())
-}
-
-func addCpCommand(rootCmd *cobra.Command) {
- rootCmd.AddCommand(newCpCommand())
+ rootCmd.AddCommand(apparmor.NewApparmorCommand())
}
diff --git a/cmd/nerdctl/main_linux_test.go b/cmd/nerdctl/main_linux_test.go
deleted file mode 100644
index a61fa50ee2b..00000000000
--- a/cmd/nerdctl/main_linux_test.go
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-// TestIssue108 tests https://github.com/containerd/nerdctl/issues/108
-// ("`nerdctl run --net=host -it` fails while `nerdctl run -it --net=host` works")
-func TestIssue108(t *testing.T) {
- base := testutil.NewBase(t)
- // unbuffer(1) emulates tty, which is required by `nerdctl run -t`.
- // unbuffer(1) can be installed with `apt-get install expect`.
- unbuffer := []string{"unbuffer"}
- base.CmdWithHelper(unbuffer, "run", "-it", "--rm", "--net=host", testutil.AlpineImage,
- "echo", "this was always working").AssertOK()
- base.CmdWithHelper(unbuffer, "run", "--rm", "--net=host", "-it", testutil.AlpineImage,
- "echo", "this was not working due to issue #108").AssertOK()
-}
diff --git a/cmd/nerdctl/main_test.go b/cmd/nerdctl/main_test.go
index 03a09360147..e2ca845e490 100644
--- a/cmd/nerdctl/main_test.go
+++ b/cmd/nerdctl/main_test.go
@@ -17,13 +17,14 @@
package main
import (
- "os"
- "path/filepath"
+ "errors"
"testing"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
+ "github.com/containerd/containerd/v2/defaults"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
)
func TestMain(m *testing.M) {
@@ -32,59 +33,98 @@ func TestMain(m *testing.M) {
// TestUnknownCommand tests https://github.com/containerd/nerdctl/issues/487
func TestUnknownCommand(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- base.Cmd("non-existent-command").AssertFail()
- base.Cmd("non-existent-command", "info").AssertFail()
- base.Cmd("system", "non-existent-command").AssertFail()
- base.Cmd("system", "non-existent-command", "info").AssertFail()
- base.Cmd("system").AssertOK() // show help without error
- base.Cmd("system", "info").AssertOutContains("Kernel Version:")
- base.Cmd("info").AssertOutContains("Kernel Version:")
-}
+ testCase := nerdtest.Setup()
-// TestNerdctlConfig validates the configuration precedence [CLI, Env, TOML, Default].
-func TestNerdctlConfig(t *testing.T) {
- testutil.DockerIncompatible(t)
- t.Parallel()
- tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml")
- err := os.WriteFile(tomlPath, []byte(`
-snapshotter = "dummy-snapshotter-via-toml"
-`), 0400)
- assert.NilError(t, err)
- base := testutil.NewBase(t)
-
- // [Default]
- base.Cmd("info", "-f", "{{.Driver}}").AssertOutExactly(containerd.DefaultSnapshotter + "\n")
-
- // [TOML, Default]
- if len(base.Env) == 0 {
- base.Env = os.Environ()
+ var unknownSubCommand = errors.New("unknown subcommand")
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "non-existent-command",
+ Command: test.Command("non-existent-command"),
+ Expected: test.Expects(1, []error{unknownSubCommand}, nil),
+ },
+ {
+ Description: "non-existent-command info",
+ Command: test.Command("non-existent-command", "info"),
+ Expected: test.Expects(1, []error{unknownSubCommand}, nil),
+ },
+ {
+ Description: "system non-existent-command",
+ Command: test.Command("system", "non-existent-command"),
+ Expected: test.Expects(1, []error{unknownSubCommand}, nil),
+ },
+ {
+ Description: "system non-existent-command info",
+ Command: test.Command("system", "non-existent-command", "info"),
+ Expected: test.Expects(1, []error{unknownSubCommand}, nil),
+ },
+ {
+ Description: "system",
+ Command: test.Command("system"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "system info",
+ Command: test.Command("system", "info"),
+ Expected: test.Expects(0, nil, test.Contains("Kernel Version:")),
+ },
+ {
+ Description: "info",
+ Command: test.Command("info"),
+ Expected: test.Expects(0, nil, test.Contains("Kernel Version:")),
+ },
}
- base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath)
- base.Cmd("info", "-f", "{{.Driver}}").AssertOutExactly("dummy-snapshotter-via-toml\n")
- // [CLI, TOML, Default]
- base.Cmd("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli").AssertOutExactly("dummy-snapshotter-via-cli\n")
+ testCase.Run(t)
+}
- // [Env, TOML, Default]
- base.Env = append(base.Env, "CONTAINERD_SNAPSHOTTER=dummy-snapshotter-via-env")
- base.Cmd("info", "-f", "{{.Driver}}").AssertOutExactly("dummy-snapshotter-via-env\n")
+// TestNerdctlConfig validates the configuration precedence [CLI, Env, TOML, Default] and broken config rejection
+func TestNerdctlConfig(t *testing.T) {
+ testCase := nerdtest.Setup()
- // [CLI, Env, TOML, Default]
- base.Cmd("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli").AssertOutExactly("dummy-snapshotter-via-cli\n")
-}
+ // Docker does not support nerdctl.toml obviously
+ testCase.Require = test.Not(nerdtest.Docker)
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Default",
+ Command: test.Command("info", "-f", "{{.Driver}}"),
+ Expected: test.Expects(0, nil, test.Equals(defaults.DefaultSnapshotter+"\n")),
+ },
+ {
+ Description: "TOML > Default",
+ Command: test.Command("info", "-f", "{{.Driver}}"),
+ Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-toml\n")),
+ Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`),
+ },
+ {
+ Description: "Cli > TOML > Default",
+ Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"),
+ Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")),
+ Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`),
+ },
+ {
+ Description: "Env > TOML > Default",
+ Command: test.Command("info", "-f", "{{.Driver}}"),
+ Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"},
+ Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-env\n")),
+ Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`),
+ },
+ {
+ Description: "Cli > Env > TOML > Default",
+ Command: test.Command("info", "-f", "{{.Driver}}", "--snapshotter=dummy-snapshotter-via-cli"),
+ Env: map[string]string{"CONTAINERD_SNAPSHOTTER": "dummy-snapshotter-via-env"},
+ Expected: test.Expects(0, nil, test.Equals("dummy-snapshotter-via-cli\n")),
+ Config: test.WithConfig(nerdtest.NerdctlToml, `snapshotter = "dummy-snapshotter-via-toml"`),
+ },
+ {
+ Description: "Broken config",
+ Command: test.Command("info"),
+ Expected: test.Expects(1, []error{errors.New("failed to load nerdctl config")}, nil),
+ Config: test.WithConfig(nerdtest.NerdctlToml, `# containerd config, not nerdctl config
+version = 2`),
+ },
+ }
-func TestNerdctlConfigBad(t *testing.T) {
- testutil.DockerIncompatible(t)
- t.Parallel()
- tomlPath := filepath.Join(t.TempDir(), "config.toml")
- err := os.WriteFile(tomlPath, []byte(`
-# containerd config, not nerdctl config
-version = 2
-`), 0400)
- assert.NilError(t, err)
- base := testutil.NewBase(t)
- base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath)
- base.Cmd("info").AssertFail()
+ testCase.Run(t)
}
diff --git a/cmd/nerdctl/main_test_test.go b/cmd/nerdctl/main_test_test.go
new file mode 100644
index 00000000000..695c52bac07
--- /dev/null
+++ b/cmd/nerdctl/main_test_test.go
@@ -0,0 +1,105 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package main
+
+import (
+ "errors"
+ "log"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+// TestTest is testing the test tooling itself
+func TestTest(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "failure",
+ Command: test.Command("undefinedcommand"),
+ Expected: test.Expects(1, nil, nil),
+ },
+ {
+ Description: "success",
+ Command: test.Command("info"),
+ Expected: test.Expects(0, nil, nil),
+ },
+ {
+ Description: "failure with single error testing",
+ Command: test.Command("undefinedcommand"),
+ Expected: test.Expects(1, []error{errors.New("unknown subcommand")}, nil),
+ },
+ {
+ Description: "success with contains output testing",
+ Command: test.Command("info"),
+ Expected: test.Expects(0, nil, test.Contains("Kernel")),
+ },
+ {
+ Description: "success with negative output testing",
+ Command: test.Command("info"),
+ Expected: test.Expects(0, nil, test.DoesNotContain("foobar")),
+ },
+ // Note that docker annoyingly returns 125 in a few conditions like this
+ {
+ Description: "failure with multiple error testing",
+ Command: test.Command("-fail"),
+ Expected: test.Expects(-1, []error{errors.New("unknown"), errors.New("shorthand")}, nil),
+ },
+ {
+ Description: "success with exact output testing",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("echo", "foobar")
+ },
+ Expected: test.Expects(0, nil, test.Equals("foobar\n")),
+ },
+ {
+ Description: "data propagation",
+ Data: test.WithData("status", "uninitialized"),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ data.Set("status", data.Get("status")+"-setup")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Custom("printf", data.Get("status"))
+ data.Set("status", data.Get("status")+"-command")
+ return cmd
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ if data.Get("status") == "uninitialized" {
+ return
+ }
+ if data.Get("status") != "uninitialized-setup-command" {
+ log.Fatalf("unexpected status label %q", data.Get("status"))
+ }
+ data.Set("status", data.Get("status")+"-cleanup")
+ },
+ SubTests: []*test.Case{
+ {
+ Description: "Subtest data propagation",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("printf", data.Get("status"))
+ },
+ Expected: test.Expects(0, nil, test.Equals("uninitialized-setup-command")),
+ },
+ },
+ Expected: test.Expects(0, nil, test.Equals("uninitialized-setup")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/main_windows.go b/cmd/nerdctl/main_windows.go
index e8c48782a20..391d34cfeed 100644
--- a/cmd/nerdctl/main_windows.go
+++ b/cmd/nerdctl/main_windows.go
@@ -24,22 +24,6 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool {
return false
}
-func shellCompleteNamespaceNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
-
-func shellCompleteSnapshotterNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
-
-func shellCompleteCgroupManagerNames(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return nil, cobra.ShellCompDirectiveNoFileComp
-}
-
func addApparmorCommand(rootCmd *cobra.Command) {
// NOP
}
-
-func addCpCommand(rootCmd *cobra.Command) {
- // NOP
-}
diff --git a/cmd/nerdctl/namespace.go b/cmd/nerdctl/namespace/namespace.go
similarity index 83%
rename from cmd/nerdctl/namespace.go
rename to cmd/nerdctl/namespace/namespace.go
index 6ede2e28aa3..6885619d341 100644
--- a/cmd/nerdctl/namespace.go
+++ b/cmd/nerdctl/namespace/namespace.go
@@ -14,30 +14,32 @@
limitations under the License.
*/
-package main
+package namespace
import (
"fmt"
- "os"
"sort"
"strings"
"text/tabwriter"
- "github.com/containerd/containerd/namespaces"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ "github.com/containerd/containerd/v2/pkg/namespaces"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
)
-func newNamespaceCommand() *cobra.Command {
+func NewNamespaceCommand() *cobra.Command {
namespaceCommand := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "namespace",
Aliases: []string{"ns"},
Short: "Manage containerd namespaces",
Long: "Unrelated to Linux namespaces and Kubernetes namespaces",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
@@ -63,7 +65,7 @@ func newNamespaceLsCommand() *cobra.Command {
}
func namespaceLsAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -103,27 +105,24 @@ func namespaceLsAction(cmd *cobra.Command, args []string) error {
containers, err := client.Containers(ctx)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
}
numContainers = len(containers)
images, err := client.ImageService().List(ctx)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
}
numImages = len(images)
- volStore, err := volumestore.Path(dataStore, ns)
+ volStore, err := volumestore.New(dataStore, ns)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
} else {
- volEnts, err := os.ReadDir(volStore)
+ numVolumes, err = volStore.Count()
if err != nil {
- if !os.IsNotExist(err) {
- logrus.Warn(err)
- }
+ log.L.Warn(err)
}
- numVolumes = len(volEnts)
}
labels, err := client.NamespaceService().Labels(ctx, ns)
diff --git a/cmd/nerdctl/namespace_create.go b/cmd/nerdctl/namespace/namespace_create.go
similarity index 86%
rename from cmd/nerdctl/namespace_create.go
rename to cmd/nerdctl/namespace/namespace_create.go
index 2d8d6617137..4c6e51312fb 100644
--- a/cmd/nerdctl/namespace_create.go
+++ b/cmd/nerdctl/namespace/namespace_create.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package namespace
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/namespace"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/namespace"
)
func newNamespaceCreateCommand() *cobra.Command {
@@ -38,7 +40,7 @@ func newNamespaceCreateCommand() *cobra.Command {
}
func processNamespaceCreateCommandOption(cmd *cobra.Command) (types.NamespaceCreateOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.NamespaceCreateOptions{}, err
}
diff --git a/cmd/nerdctl/namespace_inspect.go b/cmd/nerdctl/namespace/namespace_inspect.go
similarity index 88%
rename from cmd/nerdctl/namespace_inspect.go
rename to cmd/nerdctl/namespace/namespace_inspect.go
index b308c95b93f..cd3a5ff98e0 100644
--- a/cmd/nerdctl/namespace_inspect.go
+++ b/cmd/nerdctl/namespace/namespace_inspect.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package namespace
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/namespace"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/namespace"
)
func newNamespaceInspectCommand() *cobra.Command {
@@ -40,7 +42,7 @@ func newNamespaceInspectCommand() *cobra.Command {
}
func processNamespaceInspectOptions(cmd *cobra.Command) (types.NamespaceInspectOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.NamespaceInspectOptions{}, err
}
diff --git a/cmd/nerdctl/namespace_remove.go b/cmd/nerdctl/namespace/namespace_remove.go
similarity index 86%
rename from cmd/nerdctl/namespace_remove.go
rename to cmd/nerdctl/namespace/namespace_remove.go
index 7f23031b581..b665736b1d8 100644
--- a/cmd/nerdctl/namespace_remove.go
+++ b/cmd/nerdctl/namespace/namespace_remove.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package namespace
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/namespace"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/namespace"
)
func newNamespaceRmCommand() *cobra.Command {
@@ -38,7 +40,7 @@ func newNamespaceRmCommand() *cobra.Command {
}
func processNamespaceRemoveOptions(cmd *cobra.Command) (types.NamespaceRemoveOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.NamespaceRemoveOptions{}, err
}
diff --git a/cmd/nerdctl/namespace/namespace_test.go b/cmd/nerdctl/namespace/namespace_test.go
new file mode 100644
index 00000000000..bad2c633371
--- /dev/null
+++ b/cmd/nerdctl/namespace/namespace_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package namespace
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/namespace_update.go b/cmd/nerdctl/namespace/namespace_update.go
similarity index 86%
rename from cmd/nerdctl/namespace_update.go
rename to cmd/nerdctl/namespace/namespace_update.go
index fb58ae6edcc..b390dc26792 100644
--- a/cmd/nerdctl/namespace_update.go
+++ b/cmd/nerdctl/namespace/namespace_update.go
@@ -14,13 +14,15 @@
limitations under the License.
*/
-package main
+package namespace
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/namespace"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/namespace"
)
func newNamespacelabelUpdateCommand() *cobra.Command {
@@ -37,7 +39,7 @@ func newNamespacelabelUpdateCommand() *cobra.Command {
}
func processNamespaceUpdateCommandOption(cmd *cobra.Command) (types.NamespaceUpdateOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.NamespaceUpdateOptions{}, err
}
diff --git a/cmd/nerdctl/network.go b/cmd/nerdctl/network/network.go
similarity index 80%
rename from cmd/nerdctl/network.go
rename to cmd/nerdctl/network/network.go
index cc9b38d5f51..b953135cd0e 100644
--- a/cmd/nerdctl/network.go
+++ b/cmd/nerdctl/network/network.go
@@ -14,18 +14,20 @@
limitations under the License.
*/
-package main
+package network
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newNetworkCommand() *cobra.Command {
+func NewNetworkCommand() *cobra.Command {
networkCommand := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "network",
Short: "Manage networks",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
diff --git a/cmd/nerdctl/network_create.go b/cmd/nerdctl/network/network_create.go
similarity index 66%
rename from cmd/nerdctl/network_create.go
rename to cmd/nerdctl/network/network_create.go
index c7452277bdf..1b996d27f4d 100644
--- a/cmd/nerdctl/network_create.go
+++ b/cmd/nerdctl/network/network_create.go
@@ -14,18 +14,19 @@
limitations under the License.
*/
-package main
+package network
import (
"fmt"
- "github.com/containerd/containerd/identifiers"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/network"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/strutil"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
+ "github.com/containerd/nerdctl/v2/pkg/identifiers"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func newNetworkCreateCommand() *cobra.Command {
@@ -33,35 +34,33 @@ func newNetworkCreateCommand() *cobra.Command {
Use: "create [flags] NETWORK",
Short: "Create a network",
Long: `NOTE: To isolate CNI bridge, CNI plugin "firewall" (>= v1.1.0) is needed.`,
- Args: IsExactArgs(1),
+ Args: helpers.IsExactArgs(1),
RunE: networkCreateAction,
SilenceUsage: true,
SilenceErrors: true,
}
networkCreateCommand.Flags().StringP("driver", "d", DefaultNetworkDriver, "Driver to manage the Network")
- networkCreateCommand.RegisterFlagCompletionFunc("driver", shellCompleteNetworkDrivers)
+ networkCreateCommand.RegisterFlagCompletionFunc("driver", completion.NetworkDrivers)
networkCreateCommand.Flags().StringArrayP("opt", "o", nil, "Set driver specific options")
- networkCreateCommand.Flags().String("ipam-driver", "default", "IP Address Management Driver")
- networkCreateCommand.RegisterFlagCompletionFunc("ipam-driver", shellCompleteIPAMDrivers)
+ networkCreateCommand.Flags().String("ipam-driver", "default", "IP Address helpers.Management Driver")
+ networkCreateCommand.RegisterFlagCompletionFunc("ipam-driver", completion.IPAMDrivers)
networkCreateCommand.Flags().StringArray("ipam-opt", nil, "Set IPAM driver specific options")
- networkCreateCommand.Flags().String("subnet", "", `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
+ networkCreateCommand.Flags().StringArray("subnet", nil, `Subnet in CIDR format that represents a network segment, e.g. "10.5.0.0/16"`)
networkCreateCommand.Flags().String("gateway", "", `Gateway for the master subnet`)
networkCreateCommand.Flags().String("ip-range", "", `Allocate container ip from a sub-range`)
networkCreateCommand.Flags().StringArray("label", nil, "Set metadata for a network")
+ networkCreateCommand.Flags().Bool("ipv6", false, "Enable IPv6 networking")
return networkCreateCommand
}
func networkCreateAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
name := args[0]
- if err := identifiers.Validate(name); err != nil {
- return fmt.Errorf("malformed name %s: %w", name, err)
- }
- if err != nil {
- return err
+ if err := identifiers.ValidateDockerCompat(name); err != nil {
+ return fmt.Errorf("invalid network name: %w", err)
}
driver, err := cmd.Flags().GetString("driver")
if err != nil {
@@ -79,7 +78,7 @@ func networkCreateAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
- subnetStr, err := cmd.Flags().GetString("subnet")
+ subnets, err := cmd.Flags().GetStringArray("subnet")
if err != nil {
return err
}
@@ -96,19 +95,22 @@ func networkCreateAction(cmd *cobra.Command, args []string) error {
return err
}
labels = strutil.DedupeStrSlice(labels)
+ ipv6, err := cmd.Flags().GetBool("ipv6")
+ if err != nil {
+ return err
+ }
return network.Create(types.NetworkCreateOptions{
- GOptions: globalOptions,
- CreateOptions: netutil.CreateOptions{
- Name: name,
- Driver: driver,
- Options: strutil.ConvertKVStringsToMap(opts),
- IPAMDriver: ipamDriver,
- IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
- Subnet: subnetStr,
- Gateway: gatewayStr,
- IPRange: ipRangeStr,
- Labels: labels,
- },
+ GOptions: globalOptions,
+ Name: name,
+ Driver: driver,
+ Options: strutil.ConvertKVStringsToMap(opts),
+ IPAMDriver: ipamDriver,
+ IPAMOptions: strutil.ConvertKVStringsToMap(ipamOpts),
+ Subnets: subnets,
+ Gateway: gatewayStr,
+ IPRange: ipRangeStr,
+ Labels: labels,
+ IPv6: ipv6,
}, cmd.OutOrStdout())
}
diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go
new file mode 100644
index 00000000000..15012eabe88
--- /dev/null
+++ b/cmd/nerdctl/network/network_create_linux_test.go
@@ -0,0 +1,110 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "net"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ ipv6helper "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestNetworkCreate(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "vanilla",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("network", "create", identifier)
+ netw := nerdtest.InspectNetwork(helpers, identifier)
+ assert.Equal(t, len(netw.IPAM.Config), 1)
+ data.Set("subnet", netw.IPAM.Config[0].Subnet)
+
+ helpers.Ensure("network", "create", data.Identifier("1"))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ helpers.Anyhow("network", "rm", data.Identifier("1"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ data.Set("container2", helpers.Capture("run", "--rm", "--net", data.Identifier("1"), testutil.CommonImage, "ip", "route"))
+ return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "route")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Errors: nil,
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.Contains(stdout, data.Get("subnet")), info)
+ assert.Assert(t, !strings.Contains(data.Get("container2"), data.Get("subnet")), info)
+ },
+ }
+ },
+ },
+ {
+ Description: "with MTU",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier(), "--driver", "bridge", "--opt", "com.docker.network.driver.mtu=9216")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ifconfig", "eth0")
+ },
+ Expected: test.Expects(0, nil, test.Contains("MTU:9216")),
+ },
+ {
+ Description: "with ipv6",
+ Require: nerdtest.OnlyIPv6,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ subnetStr := "2001:db8:8::/64"
+ data.Set("subnetStr", subnetStr)
+ _, _, err := net.ParseCIDR(subnetStr)
+ assert.Assert(t, err == nil)
+
+ helpers.Ensure("network", "create", data.Identifier(), "--ipv6", "--subnet", subnetStr)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--rm", "--net", data.Identifier(), testutil.CommonImage, "ip", "addr", "show", "dev", "eth0")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ _, subnet, _ := net.ParseCIDR(data.Get("subnetStr"))
+ ip := ipv6helper.FindIPv6(stdout)
+ assert.Assert(t, subnet.Contains(ip), info)
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/network/network_create_unix.go b/cmd/nerdctl/network/network_create_unix.go
new file mode 100644
index 00000000000..dbfb8a9781c
--- /dev/null
+++ b/cmd/nerdctl/network/network_create_unix.go
@@ -0,0 +1,21 @@
+//go:build unix
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+const DefaultNetworkDriver = "bridge"
diff --git a/cmd/nerdctl/network/network_create_windows.go b/cmd/nerdctl/network/network_create_windows.go
new file mode 100644
index 00000000000..1be4a08f4e4
--- /dev/null
+++ b/cmd/nerdctl/network/network_create_windows.go
@@ -0,0 +1,19 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+const DefaultNetworkDriver = "nat"
diff --git a/cmd/nerdctl/network_inspect.go b/cmd/nerdctl/network/network_inspect.go
similarity index 88%
rename from cmd/nerdctl/network_inspect.go
rename to cmd/nerdctl/network/network_inspect.go
index cea18685fcd..6a0c0a6bc53 100644
--- a/cmd/nerdctl/network_inspect.go
+++ b/cmd/nerdctl/network/network_inspect.go
@@ -14,12 +14,15 @@
limitations under the License.
*/
-package main
+package network
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/network"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
)
func newNetworkInspectCommand() *cobra.Command {
@@ -44,7 +47,7 @@ func newNetworkInspectCommand() *cobra.Command {
}
func networkInspectAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -68,5 +71,5 @@ func networkInspectAction(cmd *cobra.Command, args []string) error {
func networkInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show network names, including "bridge"
exclude := []string{"host", "none"}
- return shellCompleteNetworkNames(cmd, exclude)
+ return completion.NetworkNames(cmd, exclude)
}
diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go
new file mode 100644
index 00000000000..d493db498fd
--- /dev/null
+++ b/cmd/nerdctl/network/network_inspect_test.go
@@ -0,0 +1,281 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "encoding/json"
+ "errors"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestNetworkInspect(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ const (
+ testSubnet = "10.24.24.0/24"
+ testGateway = "10.24.24.1"
+ testIPRange = "10.24.24.0/25"
+ )
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier("basenet"))
+ data.Set("basenet", data.Identifier("basenet"))
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier("basenet"))
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "non existent network",
+ Command: test.Command("network", "inspect", "nonexistent"),
+ // FIXME: where is this error even comin from?
+ Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil),
+ },
+ {
+ Description: "invalid name network",
+ Command: test.Command("network", "inspect", "∞"),
+ // FIXME: this is not even a valid identifier
+ Expected: test.Expects(1, []error{errors.New("no network found matching")}, nil),
+ },
+ {
+ Description: "none",
+ Require: nerdtest.NerdctlNeedsFixing("no issue opened"),
+ Command: test.Command("network", "inspect", "none"),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, "none")
+ }),
+ },
+ {
+ Description: "host",
+ Require: nerdtest.NerdctlNeedsFixing("no issue opened"),
+ Command: test.Command("network", "inspect", "host"),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, "host")
+ }),
+ },
+ {
+ Description: "bridge",
+ Require: test.Not(test.Windows),
+ Command: test.Command("network", "inspect", "bridge"),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, "bridge")
+ }),
+ },
+ {
+ Description: "nat",
+ Require: test.Windows,
+ Command: test.Command("network", "inspect", "nat"),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, "nat")
+ }),
+ },
+ {
+ Description: "custom",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", "custom")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "remove", "custom")
+ },
+ Command: test.Command("network", "inspect", "custom"),
+ Expected: test.Expects(0, nil, func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, "custom")
+ }),
+ },
+ {
+ Description: "match exact id",
+ // See notes below
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Get("basenet"), "--format", "{{ .Id }}"))
+ return helpers.Command("network", "inspect", id)
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, data.Get("basenet"))
+ },
+ }
+ },
+ },
+ {
+ Description: "match part of id",
+ // FIXME: for windows, network inspect testnetworkinspect-basenet-468cf999 --format {{ .Id }} MAY fail here
+ // This is bizarre, as it is working in the match exact id test - and there does not seem to be a particular reason for that
+ Require: test.Not(test.Windows),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Get("basenet"), "--format", "{{ .Id }}"))
+ return helpers.Command("network", "inspect", id[0:25])
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, data.Get("basenet"))
+ },
+ }
+ },
+ },
+ {
+ Description: "using another net short id",
+ // FIXME: for windows, network inspect testnetworkinspect-basenet-468cf999 --format {{ .Id }} MAY fail here
+ // This is bizarre, as it is working in the match exact id test - and there does not seem to be a particular reason for that
+ Require: test.Not(test.Windows),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Get("basenet"), "--format", "{{ .Id }}"))
+ helpers.Ensure("network", "create", id[0:12])
+ data.Set("netname", id[0:12])
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "remove", data.Get("netname"))
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "inspect", data.Get("netname"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ assert.Equal(t, dc[0].Name, data.Get("netname"))
+ },
+ }
+ },
+ },
+ {
+ Description: "basic",
+ // FIXME: IPAMConfig is not implemented on Windows yet
+ Require: test.Not(test.Windows),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", "--label", "tag=testNetwork", "--subnet", testSubnet,
+ "--gateway", testGateway, "--ip-range", testIPRange, data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "inspect", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ var dc []dockercompat.Network
+
+ err := json.Unmarshal([]byte(stdout), &dc)
+ assert.NilError(t, err, "Unable to unmarshal output\n"+info)
+ assert.Equal(t, 1, len(dc), "Unexpectedly got multiple results\n"+info)
+ got := dc[0]
+
+ assert.Equal(t, got.Name, data.Identifier(), info)
+ assert.Equal(t, got.Labels["tag"], "testNetwork", info)
+ assert.Equal(t, len(got.IPAM.Config), 1, info)
+ assert.Equal(t, got.IPAM.Config[0].Subnet, testSubnet, info)
+ assert.Equal(t, got.IPAM.Config[0].Gateway, testGateway, info)
+ assert.Equal(t, got.IPAM.Config[0].IPRange, testIPRange, info)
+ },
+ }
+ },
+ },
+ {
+ Description: "with namespace",
+ Require: test.Not(nerdtest.Docker),
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Anyhow("network", "rm", identifier)
+ helpers.Anyhow("namespace", "remove", identifier)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "create", data.Identifier())
+ },
+
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ cmd := helpers.Custom("nerdctl", "--namespace", data.Identifier())
+
+ com := cmd.Clone()
+ com.WithArgs("network", "inspect", data.Identifier())
+ com.Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("no network found")},
+ })
+
+ com = cmd.Clone()
+ com.WithArgs("network", "remove", data.Identifier())
+ com.Run(&test.Expected{
+ ExitCode: 1,
+ Errors: []error{errors.New("no network found")},
+ })
+
+ com = cmd.Clone()
+ com.WithArgs("network", "ls")
+ com.Run(&test.Expected{
+ Output: test.DoesNotContain(data.Identifier()),
+ })
+
+ com = cmd.Clone()
+ com.WithArgs("network", "prune", "-f")
+ com.Run(&test.Expected{
+ Output: test.DoesNotContain(data.Identifier()),
+ })
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/network_list.go b/cmd/nerdctl/network/network_list.go
similarity index 79%
rename from cmd/nerdctl/network_list.go
rename to cmd/nerdctl/network/network_list.go
index c45b1e6f7b1..22a10a239cd 100644
--- a/cmd/nerdctl/network_list.go
+++ b/cmd/nerdctl/network/network_list.go
@@ -14,12 +14,14 @@
limitations under the License.
*/
-package main
+package network
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/network"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
)
func newNetworkLsCommand() *cobra.Command {
@@ -33,7 +35,7 @@ func newNetworkLsCommand() *cobra.Command {
SilenceErrors: true,
}
cmd.Flags().BoolP("quiet", "q", false, "Only display network IDs")
- // Alias "-f" is reserved for "--filter"
+ cmd.Flags().StringSliceP("filter", "f", []string{}, "Provide filter values (e.g. \"name=default\")")
cmd.Flags().String("format", "", "Format the output using the given Go template, e.g, '{{json .}}'")
cmd.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json", "table", "wide"}, cobra.ShellCompDirectiveNoFileComp
@@ -42,7 +44,7 @@ func newNetworkLsCommand() *cobra.Command {
}
func networkLsAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -54,10 +56,15 @@ func networkLsAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
+ filters, err := cmd.Flags().GetStringSlice("filter")
+ if err != nil {
+ return err
+ }
return network.List(cmd.Context(), types.NetworkListOptions{
GOptions: globalOptions,
Quiet: quiet,
Format: format,
+ Filters: filters,
Stdout: cmd.OutOrStdout(),
})
}
diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go
new file mode 100644
index 00000000000..22620287a66
--- /dev/null
+++ b/cmd/nerdctl/network/network_list_linux_test.go
@@ -0,0 +1,94 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestNetworkLsFilter(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ data.Set("identifier", data.Identifier())
+ data.Set("label", "mylabel=label-1")
+ data.Set("net1", data.Identifier("1"))
+ data.Set("net2", data.Identifier("2"))
+ data.Set("netID1", helpers.Capture("network", "create", "--label="+data.Get("label"), data.Get("net1")))
+ data.Set("netID2", helpers.Capture("network", "create", data.Get("net2")))
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier("1"))
+ helpers.Anyhow("network", "rm", data.Identifier("2"))
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "filter label",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 1, info)
+ netNames := map[string]struct{}{
+ data.Get("netID1")[:12]: {},
+ }
+
+ for _, name := range lines {
+ _, ok := netNames[name]
+ assert.Assert(t, ok, info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "filter name",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("net2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 1, info)
+ netNames := map[string]struct{}{
+ data.Get("netID2")[:12]: {},
+ }
+
+ for _, name := range lines {
+ _, ok := netNames[name]
+ assert.Assert(t, ok, info)
+ }
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/network_prune.go b/cmd/nerdctl/network/network_prune.go
similarity index 83%
rename from cmd/nerdctl/network_prune.go
rename to cmd/nerdctl/network/network_prune.go
index f67996a2733..f9114fe6bbe 100644
--- a/cmd/nerdctl/network_prune.go
+++ b/cmd/nerdctl/network/network_prune.go
@@ -14,19 +14,21 @@
limitations under the License.
*/
-package main
+package network
import (
"fmt"
"strings"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/network"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
)
-var networkDriversToKeep = []string{"host", "none", DefaultNetworkDriver}
+var NetworkDriversToKeep = []string{"host", "none", DefaultNetworkDriver}
func newNetworkPruneCommand() *cobra.Command {
networkPruneCommand := &cobra.Command{
@@ -42,7 +44,7 @@ func newNetworkPruneCommand() *cobra.Command {
}
func networkPruneAction(cmd *cobra.Command, _ []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -65,7 +67,7 @@ func networkPruneAction(cmd *cobra.Command, _ []string) error {
}
options := types.NetworkPruneOptions{
GOptions: globalOptions,
- NetworkDriversToKeep: networkDriversToKeep,
+ NetworkDriversToKeep: NetworkDriversToKeep,
Stdout: cmd.OutOrStdout(),
}
diff --git a/cmd/nerdctl/network/network_prune_linux_test.go b/cmd/nerdctl/network/network_prune_linux_test.go
new file mode 100644
index 00000000000..5c1fc0bfb17
--- /dev/null
+++ b/cmd/nerdctl/network/network_prune_linux_test.go
@@ -0,0 +1,74 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestNetworkPrune(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.Private
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Prune does not collect started container network",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("network", "create", identifier)
+ helpers.Ensure("run", "-d", "--net", identifier, "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Command: test.Command("network", "prune", "-f"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.DoesNotContain(data.Identifier()),
+ }
+ },
+ },
+ {
+ Description: "Prune does collect stopped container network",
+ NoParallel: true,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier())
+ helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ helpers.Ensure("stop", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Command: test.Command("network", "prune", "-f"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Identifier()),
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/network_remove.go b/cmd/nerdctl/network/network_remove.go
similarity index 80%
rename from cmd/nerdctl/network_remove.go
rename to cmd/nerdctl/network/network_remove.go
index 4d26b579461..907908979bc 100644
--- a/cmd/nerdctl/network_remove.go
+++ b/cmd/nerdctl/network/network_remove.go
@@ -14,15 +14,17 @@
limitations under the License.
*/
-package main
+package network
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/network"
- "github.com/containerd/nerdctl/pkg/netutil"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
func newNetworkRmCommand() *cobra.Command {
@@ -41,7 +43,7 @@ func newNetworkRmCommand() *cobra.Command {
}
func networkRmAction(cmd *cobra.Command, args []string) error {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -64,5 +66,5 @@ func networkRmAction(cmd *cobra.Command, args []string) error {
func networkRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show network names, including "bridge"
exclude := []string{netutil.DefaultNetworkName, "host", "none"}
- return shellCompleteNetworkNames(cmd, exclude)
+ return completion.NetworkNames(cmd, exclude)
}
diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go
new file mode 100644
index 00000000000..f1d6e2e5876
--- /dev/null
+++ b/cmd/nerdctl/network/network_remove_linux_test.go
@@ -0,0 +1,134 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/vishvananda/netlink"
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestNetworkRemove(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.Rootful
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "Simple network remove",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ identifier := data.Identifier()
+ helpers.Ensure("network", "create", identifier)
+ data.Set("netID", nerdtest.InspectNetwork(helpers, identifier).ID)
+ helpers.Ensure("run", "--rm", "--net", identifier, "--name", identifier, testutil.CommonImage)
+ // Verity the network is here
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.Error(t, err, "Link not found", info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Network remove when linked to container",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier())
+ helpers.Ensure("run", "-d", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity)
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "rm", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Expected: test.Expects(1, []error{errors.New("is in use")}, nil),
+ },
+ {
+ Description: "Network remove by id",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier())
+ data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID)
+ helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage)
+ // Verity the network is here
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "rm", data.Get("netID"))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.Error(t, err, "Link not found", info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Network remove by short id",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier())
+ data.Set("netID", nerdtest.InspectNetwork(helpers, data.Identifier()).ID)
+ helpers.Ensure("run", "--rm", "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage)
+ // Verity the network is here
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.NilError(t, err, "failed to find network br-"+data.Get("netID")[:12], "%v")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("network", "rm", data.Get("netID")[:12])
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ _, err := netlink.LinkByName("br-" + data.Get("netID")[:12])
+ assert.Error(t, err, "Link not found", info)
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/network/network_test.go b/cmd/nerdctl/network/network_test.go
new file mode 100644
index 00000000000..245f26e3b09
--- /dev/null
+++ b/cmd/nerdctl/network/network_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package network
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/network_create_linux_test.go b/cmd/nerdctl/network_create_linux_test.go
deleted file mode 100644
index d055c10bb28..00000000000
--- a/cmd/nerdctl/network_create_linux_test.go
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestNetworkCreateWithMTU(t *testing.T) {
- testNetwork := testutil.Identifier(t)
- base := testutil.NewBase(t)
-
- args := []string{
- "network", "create", testNetwork,
- "--driver", "bridge", "--opt", "com.docker.network.driver.mtu=9216",
- }
- base.Cmd(args...).AssertOK()
- defer base.Cmd("network", "rm", testNetwork).AssertOK()
-
- base.Cmd("run", "--rm", "--net", testNetwork, testutil.AlpineImage, "ifconfig", "eth0").AssertOutContains("MTU:9216")
-}
-
-func TestNetworkCreate(t *testing.T) {
- base := testutil.NewBase(t)
- testNetwork := testutil.Identifier(t)
-
- base.Cmd("network", "create", testNetwork).AssertOK()
- defer base.Cmd("network", "rm", testNetwork).AssertOK()
-
- net := base.InspectNetwork(testNetwork)
- assert.Equal(t, len(net.IPAM.Config), 1)
-
- base.Cmd("run", "--rm", "--net", testNetwork, testutil.CommonImage, "ip", "route").AssertOutContains(net.IPAM.Config[0].Subnet)
-
- base.Cmd("network", "create", testNetwork+"-1").AssertOK()
- defer base.Cmd("network", "rm", testNetwork+"-1").AssertOK()
-
- base.Cmd("run", "--rm", "--net", testNetwork+"-1", testutil.CommonImage, "ip", "route").AssertNoOut(net.IPAM.Config[0].Subnet)
-}
diff --git a/cmd/nerdctl/network_create_unix.go b/cmd/nerdctl/network_create_unix.go
deleted file mode 100644
index 298030ba5bc..00000000000
--- a/cmd/nerdctl/network_create_unix.go
+++ /dev/null
@@ -1,34 +0,0 @@
-//go:build freebsd || linux
-
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import "github.com/spf13/cobra"
-
-const (
- DefaultNetworkDriver = "bridge"
-)
-
-func shellCompleteNetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- candidates := []string{"bridge", "macvlan", "ipvlan"}
- return candidates, cobra.ShellCompDirectiveNoFileComp
-}
-
-func shellCompleteIPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return []string{"default", "host-local", "dhcp"}, cobra.ShellCompDirectiveNoFileComp
-}
diff --git a/cmd/nerdctl/network_inspect_test.go b/cmd/nerdctl/network_inspect_test.go
deleted file mode 100644
index 76b93ec7097..00000000000
--- a/cmd/nerdctl/network_inspect_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "runtime"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestNetworkInspect(t *testing.T) {
- if runtime.GOOS == "windows" {
- t.Skip("IPAMConfig not implemented on Windows yet")
- }
-
- testNetwork := testutil.Identifier(t)
- const (
- testSubnet = "10.24.24.0/24"
- testGateway = "10.24.24.1"
- testIPRange = "10.24.24.1/25"
- )
-
- base := testutil.NewBase(t)
- defer base.Cmd("network", "rm", testNetwork).Run()
-
- args := []string{
- "network", "create", "--label", "tag=testNetwork", "--subnet", testSubnet,
- "--gateway", testGateway, "--ip-range", testIPRange,
- testNetwork,
- }
- base.Cmd(args...).AssertOK()
- got := base.InspectNetwork(testNetwork)
-
- assert.DeepEqual(base.T, testNetwork, got.Name)
-
- expectedLabels := map[string]string{
- "tag": "testNetwork",
- }
- assert.DeepEqual(base.T, expectedLabels, got.Labels)
-
- expectedIPAM := dockercompat.IPAM{
- Config: []dockercompat.IPAMConfig{
- {
- Subnet: testSubnet,
- Gateway: testGateway,
- IPRange: testIPRange,
- },
- },
- }
- assert.DeepEqual(base.T, expectedIPAM, got.IPAM)
-}
diff --git a/cmd/nerdctl/network_prune_linux_test.go b/cmd/nerdctl/network_prune_linux_test.go
deleted file mode 100644
index d872d540b26..00000000000
--- a/cmd/nerdctl/network_prune_linux_test.go
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestNetworkPrune(t *testing.T) {
- base := testutil.NewBase(t)
- testNetwork := testutil.Identifier(t)
- base.Cmd("network", "create", testNetwork).AssertOK()
- defer base.Cmd("network", "prune", "-f").Run()
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "-d", "--net", testNetwork, "--name", tID, testutil.NginxAlpineImage).AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
-
- base.Cmd("network", "prune", "-f").AssertNoOut(testNetwork)
- base.Cmd("stop", tID).AssertOK()
- base.Cmd("network", "prune", "-f").AssertOutContains(testNetwork)
-}
diff --git a/cmd/nerdctl/network_remove_linux_test.go b/cmd/nerdctl/network_remove_linux_test.go
deleted file mode 100644
index 9003f2a4bcf..00000000000
--- a/cmd/nerdctl/network_remove_linux_test.go
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/vishvananda/netlink"
- "gotest.tools/v3/assert"
-)
-
-func TestNetworkRemoveInOtherNamespace(t *testing.T) {
- if rootlessutil.IsRootless() {
- t.Skip("test skipped for remove rootless network")
- }
- if testutil.GetTarget() == testutil.Docker {
- t.Skip("test skipped for docker")
- }
- // --namespace=nerdctl-test
- base := testutil.NewBase(t)
- // --namespace=nerdctl-other
- baseOther := testutil.NewBaseWithNamespace(t, "nerdctl-other")
- networkName := testutil.Identifier(t)
-
- base.Cmd("network", "create", networkName).AssertOK()
- defer base.Cmd("network", "rm", networkName).AssertOK()
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "-d", "--net", networkName, "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
-
- // delete network in namespace nerdctl-other
- baseOther.Cmd("network", "rm", networkName).AssertFail()
-}
-
-func TestNetworkRemove(t *testing.T) {
- if rootlessutil.IsRootless() {
- t.Skip("test skipped for remove rootless network")
- }
- base := testutil.NewBase(t)
- networkName := testutil.Identifier(t)
-
- base.Cmd("network", "create", networkName).AssertOK()
- defer base.Cmd("network", "rm", networkName).Run()
-
- networkID := base.InspectNetwork(networkName).ID
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "--rm", "--net", networkName, "--name", tID, testutil.CommonImage).AssertOK()
-
- _, err := netlink.LinkByName("br-" + networkID[:12])
- assert.NilError(t, err)
-
- base.Cmd("network", "rm", networkName).AssertOK()
-
- _, err = netlink.LinkByName("br-" + networkID[:12])
- assert.Error(t, err, "Link not found")
-}
-
-func TestNetworkRemoveWhenLinkWithContainer(t *testing.T) {
- if rootlessutil.IsRootless() {
- t.Skip("test skipped for remove rootless network")
- }
- base := testutil.NewBase(t)
- networkName := testutil.Identifier(t)
-
- base.Cmd("network", "create", networkName).AssertOK()
- defer base.Cmd("network", "rm", networkName).AssertOK()
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "-d", "--net", networkName, "--name", tID, testutil.AlpineImage, "sleep", "infinity").AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
- base.Cmd("network", "rm", networkName).AssertFail()
-}
-
-func TestNetworkRemoveById(t *testing.T) {
- if rootlessutil.IsRootless() {
- t.Skip("test skipped for remove rootless network")
- }
- base := testutil.NewBase(t)
- networkName := testutil.Identifier(t)
-
- base.Cmd("network", "create", networkName).AssertOK()
- defer base.Cmd("network", "rm", networkName).Run()
-
- networkID := base.InspectNetwork(networkName).ID
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "--rm", "--net", networkName, "--name", tID, testutil.CommonImage).AssertOK()
-
- _, err := netlink.LinkByName("br-" + networkID[:12])
- assert.NilError(t, err)
-
- base.Cmd("network", "rm", networkID).AssertOK()
-
- _, err = netlink.LinkByName("br-" + networkID[:12])
- assert.Error(t, err, "Link not found")
-}
-
-func TestNetworkRemoveByShortId(t *testing.T) {
- if rootlessutil.IsRootless() {
- t.Skip("test skipped for remove rootless network")
- }
- base := testutil.NewBase(t)
- networkName := testutil.Identifier(t)
-
- base.Cmd("network", "create", networkName).AssertOK()
- defer base.Cmd("network", "rm", networkName).Run()
-
- networkID := base.InspectNetwork(networkName).ID
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "--rm", "--net", networkName, "--name", tID, testutil.CommonImage).AssertOK()
-
- _, err := netlink.LinkByName("br-" + networkID[:12])
- assert.NilError(t, err)
-
- base.Cmd("network", "rm", networkID[:12]).AssertOK()
-
- _, err = netlink.LinkByName("br-" + networkID[:12])
- assert.Error(t, err, "Link not found")
-}
diff --git a/cmd/nerdctl/system.go b/cmd/nerdctl/system/system.go
similarity index 73%
rename from cmd/nerdctl/system.go
rename to cmd/nerdctl/system/system.go
index bb9635be295..f4a41aba5ca 100644
--- a/cmd/nerdctl/system.go
+++ b/cmd/nerdctl/system/system.go
@@ -14,23 +14,27 @@
limitations under the License.
*/
-package main
+package system
-import "github.com/spf13/cobra"
+import (
+ "github.com/spf13/cobra"
-func newSystemCommand() *cobra.Command {
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+)
+
+func NewSystemCommand() *cobra.Command {
var systemCommand = &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "system",
Short: "Manage containerd",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
// versionCommand is not here
systemCommand.AddCommand(
- newEventsCommand(),
- newInfoCommand(),
+ NewEventsCommand(),
+ NewInfoCommand(),
newSystemPruneCommand(),
)
return systemCommand
diff --git a/cmd/nerdctl/system_events.go b/cmd/nerdctl/system/system_events.go
similarity index 78%
rename from cmd/nerdctl/system_events.go
rename to cmd/nerdctl/system/system_events.go
index 9455cd1ca59..69832df35ce 100644
--- a/cmd/nerdctl/system_events.go
+++ b/cmd/nerdctl/system/system_events.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package system
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/system"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/system"
)
-func newEventsCommand() *cobra.Command {
+func NewEventsCommand() *cobra.Command {
shortHelp := `Get real time events from the server`
longHelp := shortHelp + "\nNOTE: The output format is not compatible with Docker."
var eventsCommand = &cobra.Command{
@@ -39,11 +41,12 @@ func newEventsCommand() *cobra.Command {
eventsCommand.RegisterFlagCompletionFunc("format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return []string{"json"}, cobra.ShellCompDirectiveNoFileComp
})
+ eventsCommand.Flags().StringSliceP("filter", "f", []string{}, "Filter matches containers based on given conditions")
return eventsCommand
}
func processSystemEventsOptions(cmd *cobra.Command) (types.SystemEventsOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.SystemEventsOptions{}, err
}
@@ -51,10 +54,15 @@ func processSystemEventsOptions(cmd *cobra.Command) (types.SystemEventsOptions,
if err != nil {
return types.SystemEventsOptions{}, err
}
+ filters, err := cmd.Flags().GetStringSlice("filter")
+ if err != nil {
+ return types.SystemEventsOptions{}, err
+ }
return types.SystemEventsOptions{
Stdout: cmd.OutOrStdout(),
GOptions: globalOptions,
Format: format,
+ Filters: filters,
}, nil
}
diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go
new file mode 100644
index 00000000000..3dd5d30c40d
--- /dev/null
+++ b/cmd/nerdctl/system/system_events_linux_test.go
@@ -0,0 +1,100 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package system
+
+import (
+ "testing"
+ "time"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func testEventFilterExecutor(data test.Data, helpers test.Helpers) test.TestableCommand {
+ cmd := helpers.Command("events", "--filter", data.Get("filter"), "--format", "json")
+ cmd.Background(1 * time.Second)
+ helpers.Ensure("run", "--rm", testutil.CommonImage)
+ return cmd
+}
+
+func TestEventFilters(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "CapitalizedFilter",
+ Require: test.Not(nerdtest.Docker),
+ Command: testEventFilterExecutor,
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("output")),
+ }
+ },
+ Data: test.WithData("filter", "event=START").
+ Set("output", "\"Status\":\"start\""),
+ },
+ {
+ Description: "StartEventFilter",
+ Command: testEventFilterExecutor,
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("output")),
+ }
+ },
+ Data: test.WithData("filter", "event=start").
+ Set("output", "tatus\":\"start\""),
+ },
+ {
+ Description: "UnsupportedEventFilter",
+ Require: test.Not(nerdtest.Docker),
+ Command: testEventFilterExecutor,
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("output")),
+ }
+ },
+ Data: test.WithData("filter", "event=unknown").
+ Set("output", "\"Status\":\"unknown\""),
+ },
+ {
+ Description: "StatusFilter",
+ Command: testEventFilterExecutor,
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("output")),
+ }
+ },
+ Data: test.WithData("filter", "status=start").
+ Set("output", "tatus\":\"start\""),
+ },
+ {
+ Description: "UnsupportedStatusFilter",
+ Require: test.Not(nerdtest.Docker),
+ Command: testEventFilterExecutor,
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Contains(data.Get("output")),
+ }
+ },
+ Data: test.WithData("filter", "status=unknown").
+ Set("output", "\"Status\":\"unknown\""),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/system_info.go b/cmd/nerdctl/system/system_info.go
similarity index 88%
rename from cmd/nerdctl/system_info.go
rename to cmd/nerdctl/system/system_info.go
index 1a8c94304a9..10eeed9cd21 100644
--- a/cmd/nerdctl/system_info.go
+++ b/cmd/nerdctl/system/system_info.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package system
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/system"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/system"
)
-func newInfoCommand() *cobra.Command {
+func NewInfoCommand() *cobra.Command {
var infoCommand = &cobra.Command{
Use: "info",
Args: cobra.NoArgs,
@@ -44,7 +46,7 @@ func newInfoCommand() *cobra.Command {
}
func processInfoOptions(cmd *cobra.Command) (types.SystemInfoOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.SystemInfoOptions{}, err
}
diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go
new file mode 100644
index 00000000000..dc4af4b0381
--- /dev/null
+++ b/cmd/nerdctl/system/system_info_test.go
@@ -0,0 +1,76 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package system
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func testInfoComparator(stdout string, info string, t *testing.T) {
+ var dinf dockercompat.Info
+ err := json.Unmarshal([]byte(stdout), &dinf)
+ assert.NilError(t, err, "failed to unmarshal stdout"+info)
+ unameM := infoutil.UnameM()
+ assert.Assert(t, dinf.Architecture == unameM, fmt.Sprintf("expected info.Architecture to be %q, got %q", unameM, dinf.Architecture)+info)
+}
+
+func TestInfo(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "info",
+ Command: test.Command("info", "--format", "{{json .}}"),
+ Expected: test.Expects(0, nil, testInfoComparator),
+ },
+ {
+ Description: "info convenience form",
+ Command: test.Command("info", "--format", "json"),
+ Expected: test.Expects(0, nil, testInfoComparator),
+ },
+ {
+ Description: "info with namespace",
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("nerdctl", "info")
+ },
+ Expected: test.Expects(0, nil, test.Contains("Namespace: default")),
+ },
+ {
+ Description: "info with namespace env var",
+ Env: map[string]string{
+ "CONTAINERD_NAMESPACE": "test",
+ },
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Custom("nerdctl", "info")
+ },
+ Expected: test.Expects(0, nil, test.Contains("Namespace: test")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/system_prune.go b/cmd/nerdctl/system/system_prune.go
similarity index 83%
rename from cmd/nerdctl/system_prune.go
rename to cmd/nerdctl/system/system_prune.go
index eea6031b609..29a43cb187e 100644
--- a/cmd/nerdctl/system_prune.go
+++ b/cmd/nerdctl/system/system_prune.go
@@ -14,17 +14,22 @@
limitations under the License.
*/
-package main
+package system
import (
"fmt"
"strings"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/system"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/builder"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/network"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/system"
)
func newSystemPruneCommand() *cobra.Command {
@@ -43,7 +48,7 @@ func newSystemPruneCommand() *cobra.Command {
}
func processSystemPruneOptions(cmd *cobra.Command) (types.SystemPruneOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.SystemPruneOptions{}, err
}
@@ -58,9 +63,9 @@ func processSystemPruneOptions(cmd *cobra.Command) (types.SystemPruneOptions, er
return types.SystemPruneOptions{}, err
}
- buildkitHost, err := getBuildkitHost(cmd, globalOptions.Namespace)
+ buildkitHost, err := builder.GetBuildkitHost(cmd, globalOptions.Namespace)
if err != nil {
- logrus.WithError(err).Warn("BuildKit is not running. Build caches will not be pruned.")
+ log.L.WithError(err).Warn("BuildKit is not running. Build caches will not be pruned.")
buildkitHost = ""
}
@@ -71,7 +76,7 @@ func processSystemPruneOptions(cmd *cobra.Command) (types.SystemPruneOptions, er
All: all,
Volumes: vFlag,
BuildKitHost: buildkitHost,
- NetworkDriversToKeep: networkDriversToKeep,
+ NetworkDriversToKeep: network.NetworkDriversToKeep,
}, nil
}
diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go
new file mode 100644
index 00000000000..1d47b26690c
--- /dev/null
+++ b/cmd/nerdctl/system/system_prune_linux_test.go
@@ -0,0 +1,94 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package system
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestSystemPrune(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.NoParallel = true
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "volume prune all success",
+ // Private because of prune evidently
+ Require: nerdtest.Private,
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("network", "create", data.Identifier())
+ helpers.Ensure("volume", "create", data.Identifier())
+ anonIdentifier := helpers.Capture("volume", "create")
+ helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()),
+ "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage)
+
+ data.Set("anonIdentifier", anonIdentifier)
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("network", "rm", data.Identifier())
+ helpers.Anyhow("volume", "rm", data.Identifier())
+ helpers.Anyhow("volume", "rm", data.Get("anonIdentifier"))
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ },
+ Command: test.Command("system", "prune", "-f", "--volumes", "--all"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 0,
+ Output: func(stdout string, info string, t *testing.T) {
+ volumes := helpers.Capture("volume", "ls")
+ networks := helpers.Capture("network", "ls")
+ images := helpers.Capture("images")
+ containers := helpers.Capture("ps", "-a")
+ assert.Assert(t, strings.Contains(volumes, data.Identifier()), volumes)
+ assert.Assert(t, !strings.Contains(volumes, data.Get("anonIdentifier")), volumes)
+ assert.Assert(t, !strings.Contains(containers, data.Identifier()), containers)
+ assert.Assert(t, !strings.Contains(networks, data.Identifier()), networks)
+ assert.Assert(t, !strings.Contains(images, testutil.CommonImage), images)
+ },
+ }
+ },
+ },
+ {
+ Description: "buildkit",
+ // FIXME: using a dedicated namespace does not work with rootful (because of buildkitd)
+ NoParallel: true,
+ // buildkitd is not available with docker
+ Require: test.Require(nerdtest.Build, test.Not(nerdtest.Docker)),
+ // FIXME: this test will happily say "green" even if the command actually fails to do its duty
+ // if there is nothing in the build cache.
+ // Ensure with setup here that we DO build something first
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("system", "prune", "-f", "--volumes", "--all")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return nerdtest.BuildCtlCommand(helpers, "du")
+ },
+ Expected: test.Expects(0, nil, test.Contains("Total:\t\t0B")),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/system/system_test.go b/cmd/nerdctl/system/system_test.go
new file mode 100644
index 00000000000..2c066490dd6
--- /dev/null
+++ b/cmd/nerdctl/system/system_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package system
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/system_info_test.go b/cmd/nerdctl/system_info_test.go
deleted file mode 100644
index 7606b0ed601..00000000000
--- a/cmd/nerdctl/system_info_test.go
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func testInfoJSON(stdout string) error {
- var info dockercompat.Info
- if err := json.Unmarshal([]byte(stdout), &info); err != nil {
- return err
- }
- unameM := infoutil.UnameM()
- if info.Architecture != unameM {
- return fmt.Errorf("expected info.Architecture to be %q, got %q", unameM, info.Architecture)
- }
- return nil
-}
-
-func TestInfo(t *testing.T) {
- base := testutil.NewBase(t)
- base.Cmd("info", "--format", "{{json .}}").AssertOutWithFunc(testInfoJSON)
-}
-
-func TestInfoConvenienceForm(t *testing.T) {
- testutil.DockerIncompatible(t) // until https://github.com/docker/cli/pull/3355 gets merged
- base := testutil.NewBase(t)
- base.Cmd("info", "--format", "json").AssertOutWithFunc(testInfoJSON)
-}
-
-func TestInfoWithNamespace(t *testing.T) {
- testutil.DockerIncompatible(t)
- base := testutil.NewBase(t)
- base.Args = nil // unset "--namespace=nerdctl-test"
-
- base.Cmd("info").AssertOutContains("Namespace: default")
-
- base.Env = append(os.Environ(), "CONTAINERD_NAMESPACE=test")
- base.Cmd("info").AssertOutContains("Namespace: test")
-}
diff --git a/cmd/nerdctl/system_prune_linux_test.go b/cmd/nerdctl/system_prune_linux_test.go
deleted file mode 100644
index 361698d0954..00000000000
--- a/cmd/nerdctl/system_prune_linux_test.go
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "bytes"
- "fmt"
- "io"
- "os"
- "os/exec"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/testutil"
- "github.com/sirupsen/logrus"
-)
-
-func TestSystemPrune(t *testing.T) {
- testutil.RequiresBuild(t)
- base := testutil.NewBase(t)
- base.Cmd("container", "prune", "-f").AssertOK()
- base.Cmd("network", "prune", "-f").AssertOK()
- base.Cmd("volume", "prune", "-f").AssertOK()
- base.Cmd("image", "prune", "-f", "--all").AssertOK()
-
- nID := testutil.Identifier(t)
- base.Cmd("network", "create", nID).AssertOK()
- defer base.Cmd("network", "rm", nID).Run()
-
- vID := testutil.Identifier(t)
- base.Cmd("volume", "create", vID).AssertOK()
- defer base.Cmd("volume", "rm", vID).Run()
-
- tID := testutil.Identifier(t)
- base.Cmd("run", "-v", fmt.Sprintf("%s:/volume", vID), "--net", nID,
- "--name", tID, testutil.CommonImage).AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
-
- base.Cmd("ps", "-a").AssertOutContains(tID)
- base.Cmd("images").AssertOutContains(testutil.ImageRepo(testutil.CommonImage))
-
- base.Cmd("system", "prune", "-f", "--volumes", "--all").AssertOK()
- base.Cmd("volume", "ls").AssertNoOut(vID)
- base.Cmd("ps", "-a").AssertNoOut(tID)
- base.Cmd("network", "ls").AssertNoOut(nID)
- base.Cmd("images").AssertNoOut(testutil.ImageRepo(testutil.CommonImage))
-
- if testutil.GetTarget() != testutil.Nerdctl {
- t.Skip("test skipped for buildkitd is not available with docker-compatible tests")
- }
-
- buildctlBinary, err := buildkitutil.BuildctlBinary()
- if err != nil {
- t.Fatal(err)
- }
- host, err := buildkitutil.GetBuildkitHost(testutil.Namespace)
- if err != nil {
- t.Fatal(err)
- }
-
- buildctlArgs := buildkitutil.BuildctlBaseArgs(host)
- buildctlArgs = append(buildctlArgs, "du")
- logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs)
- buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
- buildctlCmd.Env = os.Environ()
- stdout := bytes.NewBuffer(nil)
- buildctlCmd.Stdout = stdout
- if err := buildctlCmd.Run(); err != nil {
- t.Fatal(err)
- }
- readAll, err := io.ReadAll(stdout)
- if err != nil {
- t.Fatal(err)
- }
- if !strings.Contains(string(readAll), "Total:\t\t0B") {
- t.Errorf("buildkit cache is not pruned: %s", string(readAll))
- }
-}
diff --git a/cmd/nerdctl/version.go b/cmd/nerdctl/version.go
index d74444a2e4b..627c8b1940a 100644
--- a/cmd/nerdctl/version.go
+++ b/cmd/nerdctl/version.go
@@ -23,12 +23,16 @@ import (
"os"
"text/template"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
"github.com/spf13/cobra"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
func newVersionCommand() *cobra.Command {
@@ -50,7 +54,7 @@ func newVersionCommand() *cobra.Command {
func versionAction(cmd *cobra.Command, args []string) error {
var w io.Writer = os.Stdout
var tmpl *template.Template
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return err
}
@@ -66,17 +70,27 @@ func versionAction(cmd *cobra.Command, args []string) error {
}
}
- v, vErr := versionInfo(cmd, globalOptions)
+ address := globalOptions.Address
+ // rootless `nerdctl version` runs in the host namespaces, so the address is different
+ if rootlessutil.IsRootless() {
+ address, err = rootlessutil.RootlessContainredSockAddress()
+ if err != nil {
+ log.L.WithError(err).Warning("failed to inspect the rootless containerd socket address")
+ address = ""
+ }
+ }
+
+ v, vErr := versionInfo(cmd, globalOptions.Namespace, address)
if tmpl != nil {
var b bytes.Buffer
if err := tmpl.Execute(&b, v); err != nil {
return err
}
- if _, err := fmt.Fprintf(w, b.String()+"\n"); err != nil {
+ if _, err := fmt.Fprintln(w, b.String()); err != nil {
return err
}
} else {
- fmt.Fprintf(w, "Client:\n")
+ fmt.Fprintln(w, "Client:")
fmt.Fprintf(w, " Version:\t%s\n", v.Client.Version)
fmt.Fprintf(w, " OS/Arch:\t%s/%s\n", v.Client.Os, v.Client.Arch)
fmt.Fprintf(w, " Git commit:\t%s\n", v.Client.GitCommit)
@@ -88,8 +102,8 @@ func versionAction(cmd *cobra.Command, args []string) error {
}
}
if v.Server != nil {
- fmt.Fprintf(w, "\n")
- fmt.Fprintf(w, "Server:\n")
+ fmt.Fprintln(w)
+ fmt.Fprintln(w, "Server:")
for _, compo := range v.Server.Components {
fmt.Fprintf(w, " %s:\n", compo.Name)
fmt.Fprintf(w, " Version:\t%s\n", compo.Version)
@@ -102,13 +116,16 @@ func versionAction(cmd *cobra.Command, args []string) error {
return vErr
}
-// versionInfo may return partial VersionInfo on error
-func versionInfo(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (dockercompat.VersionInfo, error) {
-
+// versionInfo may return partial VersionInfo on error.
+// Address can be empty to skip inspecting the server.
+func versionInfo(cmd *cobra.Command, ns, address string) (dockercompat.VersionInfo, error) {
v := dockercompat.VersionInfo{
Client: infoutil.ClientVersion(),
}
- client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
+ if address == "" {
+ return v, nil
+ }
+ client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), ns, address)
if err != nil {
return v, err
}
diff --git a/cmd/nerdctl/volume.go b/cmd/nerdctl/volume/volume.go
similarity index 80%
rename from cmd/nerdctl/volume.go
rename to cmd/nerdctl/volume/volume.go
index b34d1cd471d..352993096b9 100644
--- a/cmd/nerdctl/volume.go
+++ b/cmd/nerdctl/volume/volume.go
@@ -14,18 +14,20 @@
limitations under the License.
*/
-package main
+package volume
import (
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
)
-func newVolumeCommand() *cobra.Command {
+func NewVolumeCommand() *cobra.Command {
volumeCommand := &cobra.Command{
- Annotations: map[string]string{Category: Management},
+ Annotations: map[string]string{helpers.Category: helpers.Management},
Use: "volume",
Short: "Manage volumes",
- RunE: unknownSubcommandAction,
+ RunE: helpers.UnknownSubcommandAction,
SilenceUsage: true,
SilenceErrors: true,
}
diff --git a/cmd/nerdctl/volume_create.go b/cmd/nerdctl/volume/volume_create.go
similarity index 69%
rename from cmd/nerdctl/volume_create.go
rename to cmd/nerdctl/volume/volume_create.go
index 5122e60d465..6927c4a556e 100644
--- a/cmd/nerdctl/volume_create.go
+++ b/cmd/nerdctl/volume/volume_create.go
@@ -14,20 +14,25 @@
limitations under the License.
*/
-package main
+package volume
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
+ "fmt"
"github.com/spf13/cobra"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
func newVolumeCreateCommand() *cobra.Command {
volumeCreateCommand := &cobra.Command{
- Use: "create [flags] VOLUME",
+ Use: "create [flags] [VOLUME]",
Short: "Create a volume",
- Args: IsExactArgs(1),
+ Args: cobra.MaximumNArgs(1),
RunE: volumeCreateAction,
SilenceUsage: true,
SilenceErrors: true,
@@ -37,7 +42,7 @@ func newVolumeCreateCommand() *cobra.Command {
}
func processVolumeCreateOptions(cmd *cobra.Command) (types.VolumeCreateOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.VolumeCreateOptions{}, err
}
@@ -45,6 +50,12 @@ func processVolumeCreateOptions(cmd *cobra.Command) (types.VolumeCreateOptions,
if err != nil {
return types.VolumeCreateOptions{}, err
}
+ for _, label := range labels {
+ if label == "" {
+ return types.VolumeCreateOptions{}, fmt.Errorf("labels cannot be empty (%w)", errdefs.ErrInvalidArgument)
+ }
+ }
+
return types.VolumeCreateOptions{
GOptions: globalOptions,
Labels: labels,
@@ -55,7 +66,13 @@ func processVolumeCreateOptions(cmd *cobra.Command) (types.VolumeCreateOptions,
func volumeCreateAction(cmd *cobra.Command, args []string) error {
options, err := processVolumeCreateOptions(cmd)
if err != nil {
- return nil
+ return err
+ }
+ volumeName := ""
+ if len(args) > 0 {
+ volumeName = args[0]
}
- return volume.Create(args[0], options)
+ _, err = volume.Create(volumeName, options)
+
+ return err
}
diff --git a/cmd/nerdctl/volume/volume_create_test.go b/cmd/nerdctl/volume/volume_create_test.go
new file mode 100644
index 00000000000..1cde5336199
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_create_test.go
@@ -0,0 +1,109 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "errors"
+ "regexp"
+ "testing"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestVolumeCreate(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "arg missing should create anonymous volume",
+ Command: test.Command("volume", "create"),
+ Expected: test.Expects(0, nil, test.Match(regexp.MustCompile("^[a-f0-9]{64}\n$"))),
+ },
+ {
+ Description: "invalid identifier should fail",
+ Command: test.Command("volume", "create", "∞"),
+ Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil),
+ },
+ {
+ Description: "too many args should fail",
+ Command: test.Command("volume", "create", "too", "many"),
+ Expected: test.Expects(1, []error{errors.New("at most 1 arg")}, nil),
+ },
+ {
+ Description: "success",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "create", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ {
+ Description: "success with labels",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "create", "--label", "foo1=baz1", "--label", "foo2=baz2", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ {
+ Description: "invalid labels should fail",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ // See https://github.com/containerd/nerdctl/issues/3126
+ return helpers.Command("volume", "create", "--label", "a", "--label", "", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+ // NOTE: docker returns 125 on this
+ Expected: test.Expects(-1, []error{errdefs.ErrInvalidArgument}, nil),
+ },
+ {
+ Description: "creating already existing volume should succeed",
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier())
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "create", data.Identifier())
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume_inspect.go b/cmd/nerdctl/volume/volume_inspect.go
similarity index 86%
rename from cmd/nerdctl/volume_inspect.go
rename to cmd/nerdctl/volume/volume_inspect.go
index 64ef29a35d2..422df57ab3e 100644
--- a/cmd/nerdctl/volume_inspect.go
+++ b/cmd/nerdctl/volume/volume_inspect.go
@@ -14,12 +14,15 @@
limitations under the License.
*/
-package main
+package volume
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
func newVolumeInspectCommand() *cobra.Command {
@@ -41,7 +44,7 @@ func newVolumeInspectCommand() *cobra.Command {
}
func processVolumeInspectOptions(cmd *cobra.Command) (types.VolumeInspectOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.VolumeInspectOptions{}, err
}
@@ -66,10 +69,10 @@ func volumeInspectAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
- return volume.Inspect(args, options)
+ return volume.Inspect(cmd.Context(), args, options)
}
func volumeInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show volume names
- return shellCompleteVolumeNames(cmd)
+ return completion.VolumeNames(cmd)
}
diff --git a/cmd/nerdctl/volume/volume_inspect_test.go b/cmd/nerdctl/volume/volume_inspect_test.go
new file mode 100644
index 00000000000..a7ec478b55a
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_inspect_test.go
@@ -0,0 +1,211 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func createFileWithSize(mountPoint string, size int64) error {
+ token := make([]byte, size)
+ _, _ = rand.Read(token)
+ err := os.WriteFile(filepath.Join(mountPoint, "test-file"), token, 0644)
+ return err
+}
+
+func TestVolumeInspect(t *testing.T) {
+ var size int64 = 1028
+
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+
+ "which is not always true. To be replaced by cp into the container.",
+ &test.Requirement{
+ Check: func(data test.Data, helpers test.Helpers) (bool, string) {
+ isDocker, _ := nerdtest.Docker.Check(data, helpers)
+ return !isDocker || os.Geteuid() == 0, "docker cli needs to be run as root"
+ },
+ })
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier("first"))
+ helpers.Ensure("volume", "create", "--label", "foo=fooval", "--label", "bar=barval", data.Identifier("second"))
+ // Obviously note here that if inspect code gets totally hosed, this entire suite will
+ // probably fail right here on the Setup instead of actually testing something
+ vol := nerdtest.InspectVolume(helpers, data.Identifier("first"))
+ err := createFileWithSize(vol.Mountpoint, size)
+ assert.NilError(t, err, "File creation failed")
+ data.Set("vol1", data.Identifier("first"))
+ data.Set("vol2", data.Identifier("second"))
+ }
+
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("first"))
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("second"))
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "arg missing should fail",
+ Command: test.Command("volume", "inspect"),
+ Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil),
+ },
+ {
+ Description: "invalid identifier should fail",
+ Command: test.Command("volume", "inspect", "∞"),
+ Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil),
+ },
+ {
+ Description: "non existent volume should fail",
+ Command: test.Command("volume", "inspect", "doesnotexist"),
+ Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil),
+ },
+ {
+ Description: "success",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", data.Get("vol1"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("vol1")),
+ func(stdout string, info string, t *testing.T) {
+ var dc []native.Volume
+ if err := json.Unmarshal([]byte(stdout), &dc); err != nil {
+ t.Fatal(err)
+ }
+ assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc))+info)
+ assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name)+info)
+ assert.Assert(t, dc[0].Labels == nil, fmt.Sprintf("expected labels to be nil and were %v", dc[0].Labels)+info)
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "inspect labels",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", data.Get("vol2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("vol2")),
+ func(stdout string, info string, t *testing.T) {
+ var dc []native.Volume
+ if err := json.Unmarshal([]byte(stdout), &dc); err != nil {
+ t.Fatal(err)
+ }
+ labels := *dc[0].Labels
+ assert.Assert(t, len(labels) == 2, fmt.Sprintf("two results, not %d", len(labels)))
+ assert.Assert(t, labels["foo"] == "fooval", fmt.Sprintf("label foo should be fooval, not %s", labels["foo"]))
+ assert.Assert(t, labels["bar"] == "barval", fmt.Sprintf("label bar should be barval, not %s", labels["bar"]))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "inspect size",
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", "--size", data.Get("vol1"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("vol1")),
+ func(stdout string, info string, t *testing.T) {
+ var dc []native.Volume
+ if err := json.Unmarshal([]byte(stdout), &dc); err != nil {
+ t.Fatal(err)
+ }
+ assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "multi success",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", data.Get("vol1"), data.Get("vol2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.Contains(data.Get("vol1")),
+ test.Contains(data.Get("vol2")),
+ func(stdout string, info string, t *testing.T) {
+ var dc []native.Volume
+ if err := json.Unmarshal([]byte(stdout), &dc); err != nil {
+ t.Fatal(err)
+ }
+ assert.Assert(t, len(dc) == 2, fmt.Sprintf("two results, not %d", len(dc)))
+ assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name))
+ assert.Assert(t, dc[1].Name == data.Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol2"), dc[1].Name))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "part success multi",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("vol1"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument},
+ Output: test.All(
+ test.Contains(data.Get("vol1")),
+ func(stdout string, info string, t *testing.T) {
+ var dc []native.Volume
+ if err := json.Unmarshal([]byte(stdout), &dc); err != nil {
+ t.Fatal(err)
+ }
+ assert.Assert(t, len(dc) == 1, fmt.Sprintf("one result, not %d", len(dc)))
+ assert.Assert(t, dc[0].Name == data.Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Get("vol1"), dc[0].Name))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "multi failure",
+ Command: test.Command("volume", "inspect", "invalid∞", "nonexistent"),
+ Expected: test.Expects(1, []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, nil),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume_list.go b/cmd/nerdctl/volume/volume_list.go
similarity index 82%
rename from cmd/nerdctl/volume_list.go
rename to cmd/nerdctl/volume/volume_list.go
index 847eba092e0..891a626a09c 100644
--- a/cmd/nerdctl/volume_list.go
+++ b/cmd/nerdctl/volume/volume_list.go
@@ -14,14 +14,14 @@
limitations under the License.
*/
-package main
+package volume
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
-
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
func newVolumeLsCommand() *cobra.Command {
@@ -46,7 +46,7 @@ func newVolumeLsCommand() *cobra.Command {
}
func processVolumeLsOptions(cmd *cobra.Command) (types.VolumeListOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.VolumeListOptions{}, err
}
@@ -83,11 +83,3 @@ func volumeLsAction(cmd *cobra.Command, args []string) error {
}
return volume.List(options)
}
-
-func getVolumes(cmd *cobra.Command, globalOptions types.GlobalCommandOptions) (map[string]native.Volume, error) {
- volumeSize, err := cmd.Flags().GetBool("size")
- if err != nil {
- return nil, err
- }
- return volume.Volumes(globalOptions.Namespace, globalOptions.DataRoot, globalOptions.Address, volumeSize, nil)
-}
diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go
new file mode 100644
index 00000000000..d48b35809c3
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_list_test.go
@@ -0,0 +1,401 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/tabutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestVolumeLsSize(t *testing.T) {
+ nerdtest.Setup()
+
+ tc := &test.Case{
+ Require: test.Not(nerdtest.Docker),
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier("1"))
+ helpers.Ensure("volume", "create", data.Identifier("2"))
+ helpers.Ensure("volume", "create", data.Identifier("empty"))
+ vol1 := nerdtest.InspectVolume(helpers, data.Identifier("1"))
+ vol2 := nerdtest.InspectVolume(helpers, data.Identifier("2"))
+
+ err := createFileWithSize(vol1.Mountpoint, 102400)
+ assert.NilError(t, err, "File creation failed")
+ err = createFileWithSize(vol2.Mountpoint, 204800)
+ assert.NilError(t, err, "File creation failed")
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("1"))
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("2"))
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("empty"))
+ },
+ Command: test.Command("volume", "ls", "--size"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info)
+ volSizes := map[string]string{
+ data.Identifier("1"): "100.0 KiB",
+ data.Identifier("2"): "200.0 KiB",
+ data.Identifier("empty"): "0.0 B",
+ }
+
+ var numMatches = 0
+ var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
+ var err = tab.ParseHeader(lines[0])
+ assert.NilError(t, err, info)
+
+ for _, line := range lines {
+ name, _ := tab.ReadRow(line, "VOLUME NAME")
+ size, _ := tab.ReadRow(line, "SIZE")
+ expectSize, ok := volSizes[name]
+ if !ok {
+ continue
+ }
+ assert.Assert(t, size == expectSize, fmt.Sprintf("expected size %s for volume %s, got %s", expectSize, name, size)+info)
+ numMatches++
+ }
+ assert.Assert(t, numMatches == len(volSizes), fmt.Sprintf("expected %d volumes, got: %d", len(volSizes), numMatches)+info)
+ },
+ }
+ },
+ }
+
+ tc.Run(t)
+}
+
+func TestVolumeLsFilter(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.Require = nerdtest.BrokenTest("This test assumes that the host-side of a volume can be written into, "+
+ "which is not always true. To be replaced by cp into the container.",
+ &test.Requirement{
+ Check: func(data test.Data, helpers test.Helpers) (bool, string) {
+ isDocker, _ := nerdtest.Docker.Check(data, helpers)
+ return !isDocker || os.Geteuid() == 0, "docker cli needs to be run as root"
+ },
+ })
+
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ var vol1, vol2, vol3, vol4 = data.Identifier("1"), data.Identifier("2"), data.Identifier("3"), data.Identifier("4")
+ var label1, label2, label3, label4 = "mylabel=label-1", "mylabel=label-2", "mylabel=label-3", "mylabel-group=label-4"
+
+ helpers.Ensure("volume", "create", "--label="+label1, "--label="+label4, vol1)
+ helpers.Ensure("volume", "create", "--label="+label2, "--label="+label4, vol2)
+ helpers.Ensure("volume", "create", "--label="+label3, vol3)
+ helpers.Ensure("volume", "create", vol4)
+
+ // FIXME
+ // This will not work with Docker rootful and Docker cli run as a user
+ // We should replace it with cp inside the container
+ err := createFileWithSize(nerdtest.InspectVolume(helpers, vol1).Mountpoint, 409600)
+ assert.NilError(t, err, "File creation failed")
+ err = createFileWithSize(nerdtest.InspectVolume(helpers, vol2).Mountpoint, 1024000)
+ assert.NilError(t, err, "File creation failed")
+ err = createFileWithSize(nerdtest.InspectVolume(helpers, vol3).Mountpoint, 409600)
+ assert.NilError(t, err, "File creation failed")
+ err = createFileWithSize(nerdtest.InspectVolume(helpers, vol4).Mountpoint, 1024000)
+ assert.NilError(t, err, "File creation failed")
+
+ data.Set("vol1", vol1)
+ data.Set("vol2", vol2)
+ data.Set("vol3", vol3)
+ data.Set("vol4", vol4)
+ data.Set("mainlabel", "mylabel")
+ data.Set("label1", label1)
+ data.Set("label2", label2)
+ data.Set("label3", label3)
+ data.Set("label4", label4)
+
+ }
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Get("vol1"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("vol2"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("vol3"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("vol4"))
+ }
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "No filter",
+ Command: test.Command("volume", "ls", "--quiet"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 4, "expected at least 4 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ data.Get("vol2"): {},
+ data.Get("vol3"): {},
+ data.Get("vol4"): {},
+ }
+ var numMatches = 0
+ for _, name := range lines {
+ _, ok := volNames[name]
+ if !ok {
+ continue
+ }
+ numMatches++
+ }
+ assert.Assert(t, len(volNames) == numMatches, fmt.Sprintf("expected %d volumes, got: %d", len(volNames), numMatches))
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving label=mainlabel",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ data.Get("vol2"): {},
+ data.Get("vol3"): {},
+ }
+ for _, name := range lines {
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving label=mainlabel=label2",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 1, "expected at least 1 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol2"): {},
+ }
+ for _, name := range lines {
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving label=mainlabel=",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel")+"=")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving label=mainlabel=label1 and label=mainlabel=label2",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("label1"), "--filter", "label="+data.Get("label2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, strings.TrimSpace(stdout) == "", "expected no result"+info)
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving label=mainlabel and label=grouplabel=label4",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Get("mainlabel"), "--filter", "label="+data.Get("label4"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ data.Get("vol2"): {},
+ }
+ for _, name := range lines {
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving name=volume1",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 1, "expected at least 1 line"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ }
+ for _, name := range lines {
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving name=volume1 and name=volume2",
+ // Nerdctl filter behavior is broken
+ Require: nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/3452"),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Get("vol1"), "--filter", "name="+data.Get("vol2"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 2, "expected at least 2 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ data.Get("vol2"): {},
+ }
+ for _, name := range lines {
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving size=1024000",
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--size", "--filter", "size=1024000")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol2"): {},
+ data.Get("vol4"): {},
+ }
+ var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
+ var err = tab.ParseHeader(lines[0])
+ assert.NilError(t, err, "Tab reader failed")
+ for _, line := range lines {
+
+ name, _ := tab.ReadRow(line, "VOLUME NAME")
+ if name == "VOLUME NAME" {
+ continue
+ }
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving size>=1024000 size<=2048000",
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol2"): {},
+ data.Get("vol4"): {},
+ }
+ var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
+ var err = tab.ParseHeader(lines[0])
+ assert.NilError(t, err, "Tab reader failed")
+ for _, line := range lines {
+
+ name, _ := tab.ReadRow(line, "VOLUME NAME")
+ if name == "VOLUME NAME" {
+ continue
+ }
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ {
+ Description: "Retrieving size>204800 size<1024000",
+ Require: test.Not(nerdtest.Docker),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000")
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ var lines = strings.Split(strings.TrimSpace(stdout), "\n")
+ assert.Assert(t, len(lines) >= 3, "expected at least 3 lines"+info)
+ volNames := map[string]struct{}{
+ data.Get("vol1"): {},
+ data.Get("vol3"): {},
+ }
+ var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
+ var err = tab.ParseHeader(lines[0])
+ assert.NilError(t, err, "Tab reader failed")
+ for _, line := range lines {
+
+ name, _ := tab.ReadRow(line, "VOLUME NAME")
+ if name == "VOLUME NAME" {
+ continue
+ }
+ _, ok := volNames[name]
+ assert.Assert(t, ok, fmt.Sprintf("unexpected volume %s found", name)+info)
+ }
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go
new file mode 100644
index 00000000000..c2a2d6ce3db
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_namespace_test.go
@@ -0,0 +1,106 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "testing"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestVolumeNamespace(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ // Docker does not support namespaces
+ testCase.Require = test.Not(nerdtest.Docker)
+
+ // Create a volume in a different namespace
+ testCase.Setup = func(data test.Data, helpers test.Helpers) {
+ data.Set("root_namespace", data.Identifier())
+ data.Set("root_volume", data.Identifier())
+ helpers.Ensure("--namespace", data.Identifier(), "volume", "create", data.Identifier())
+ }
+
+ // Cleanup once done
+ testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
+ if data.Get("root_namespace") != "" {
+ helpers.Anyhow("--namespace", data.Identifier(), "volume", "remove", data.Identifier())
+ helpers.Anyhow("namespace", "remove", data.Identifier())
+ }
+ }
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "inspect another namespace volume should fail",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "inspect", data.Get("root_volume"))
+ },
+ Expected: test.Expects(1, []error{
+ errdefs.ErrNotFound,
+ }, nil),
+ },
+ {
+ Description: "removing another namespace volume should fail",
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "remove", data.Get("root_volume"))
+ },
+ Expected: test.Expects(1, []error{
+ errdefs.ErrNotFound,
+ }, nil),
+ },
+ {
+ Description: "prune should leave another namespace volume untouched",
+ // Make it private so that we do not interact with other tests in the main namespace
+ Require: nerdtest.Private,
+ Command: test.Command("volume", "prune", "-a", "-f"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain(data.Get("root_volume")),
+ func(stdout string, info string, t *testing.T) {
+ helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume"))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "create with the same name should work, then delete it",
+ NoParallel: true,
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "create", data.Get("root_volume"))
+ },
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", data.Get("root_volume"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: func(stdout string, info string, t *testing.T) {
+ helpers.Ensure("volume", "inspect", data.Get("root_volume"))
+ helpers.Ensure("volume", "rm", data.Get("root_volume"))
+ helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume"))
+ },
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume_prune.go b/cmd/nerdctl/volume/volume_prune.go
similarity index 80%
rename from cmd/nerdctl/volume_prune.go
rename to cmd/nerdctl/volume/volume_prune.go
index df0cbbd680b..cbc74fb882f 100644
--- a/cmd/nerdctl/volume_prune.go
+++ b/cmd/nerdctl/volume/volume_prune.go
@@ -14,16 +14,18 @@
limitations under the License.
*/
-package main
+package volume
import (
"fmt"
"strings"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
func newVolumePruneCommand() *cobra.Command {
@@ -35,12 +37,18 @@ func newVolumePruneCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
+ volumePruneCommand.Flags().BoolP("all", "a", false, "Remove all unused volumes, not just anonymous ones")
volumePruneCommand.Flags().BoolP("force", "f", false, "Do not prompt for confirmation")
return volumePruneCommand
}
func processVolumePruneOptions(cmd *cobra.Command) (types.VolumePruneOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
+ if err != nil {
+ return types.VolumePruneOptions{}, err
+ }
+
+ all, err := cmd.Flags().GetBool("all")
if err != nil {
return types.VolumePruneOptions{}, err
}
@@ -52,6 +60,7 @@ func processVolumePruneOptions(cmd *cobra.Command) (types.VolumePruneOptions, er
options := types.VolumePruneOptions{
GOptions: globalOptions,
+ All: all,
Force: force,
Stdout: cmd.OutOrStdout(),
}
diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go
new file mode 100644
index 00000000000..c33922c730b
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_prune_linux_test.go
@@ -0,0 +1,110 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestVolumePrune(t *testing.T) {
+ var setup = func(data test.Data, helpers test.Helpers) {
+ anonIDBusy := strings.TrimSpace(helpers.Capture("volume", "create"))
+ anonIDDangling := strings.TrimSpace(helpers.Capture("volume", "create"))
+
+ namedBusy := data.Identifier("busy")
+ namedDangling := data.Identifier("free")
+
+ helpers.Ensure("volume", "create", namedBusy)
+ helpers.Ensure("volume", "create", namedDangling)
+ helpers.Ensure("run", "--name", data.Identifier(),
+ "-v", namedBusy+":/namedbusyvolume",
+ "-v", anonIDBusy+":/anonbusyvolume", testutil.CommonImage)
+
+ data.Set("anonIDBusy", anonIDBusy)
+ data.Set("anonIDDangling", anonIDDangling)
+ data.Set("namedBusy", namedBusy)
+ data.Set("namedDangling", namedDangling)
+ }
+
+ var cleanup = func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDBusy"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("anonIDDangling"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("namedBusy"))
+ helpers.Anyhow("volume", "rm", "-f", data.Get("namedDangling"))
+ }
+
+ testCase := nerdtest.Setup()
+ // This set must be marked as private, since we cannot prune without interacting with other tests.
+ testCase.Require = nerdtest.Private
+ // Furthermore, these two subtests cannot be run in parallel
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "prune anonymous only",
+ NoParallel: true,
+ Setup: setup,
+ Cleanup: cleanup,
+ Command: test.Command("volume", "prune", "-f"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain(data.Get("anonIDBusy")),
+ test.Contains(data.Get("anonIDDangling")),
+ test.DoesNotContain(data.Get("namedBusy")),
+ test.DoesNotContain(data.Get("namedDangling")),
+ func(stdout string, info string, t *testing.T) {
+ helpers.Ensure("volume", "inspect", data.Get("anonIDBusy"))
+ helpers.Fail("volume", "inspect", data.Get("anonIDDangling"))
+ helpers.Ensure("volume", "inspect", data.Get("namedBusy"))
+ helpers.Ensure("volume", "inspect", data.Get("namedDangling"))
+ },
+ ),
+ }
+ },
+ },
+ {
+ Description: "prune all",
+ NoParallel: true,
+ Setup: setup,
+ Cleanup: cleanup,
+ Command: test.Command("volume", "prune", "-f", "--all"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.All(
+ test.DoesNotContain(data.Get("anonIDBusy")),
+ test.Contains(data.Get("anonIDDangling")),
+ test.DoesNotContain(data.Get("namedBusy")),
+ test.Contains(data.Get("namedDangling")),
+ func(stdout string, info string, t *testing.T) {
+ helpers.Ensure("volume", "inspect", data.Get("anonIDBusy"))
+ helpers.Fail("volume", "inspect", data.Get("anonIDDangling"))
+ helpers.Ensure("volume", "inspect", data.Get("namedBusy"))
+ helpers.Fail("volume", "inspect", data.Get("namedDangling"))
+ },
+ ),
+ }
+ },
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume_remove.go b/cmd/nerdctl/volume/volume_remove.go
similarity index 84%
rename from cmd/nerdctl/volume_remove.go
rename to cmd/nerdctl/volume/volume_remove.go
index 71245937b47..d4350b93cee 100644
--- a/cmd/nerdctl/volume_remove.go
+++ b/cmd/nerdctl/volume/volume_remove.go
@@ -14,13 +14,16 @@
limitations under the License.
*/
-package main
+package volume
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
"github.com/spf13/cobra"
+
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
+ "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
func newVolumeRmCommand() *cobra.Command {
@@ -40,7 +43,7 @@ func newVolumeRmCommand() *cobra.Command {
}
func processVolumeRmOptions(cmd *cobra.Command) (types.VolumeRemoveOptions, error) {
- globalOptions, err := processRootCmdFlags(cmd)
+ globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
if err != nil {
return types.VolumeRemoveOptions{}, err
}
@@ -72,5 +75,5 @@ func volumeRmAction(cmd *cobra.Command, args []string) error {
func volumeRmShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// show volume names
- return shellCompleteVolumeNames(cmd)
+ return completion.VolumeNames(cmd)
}
diff --git a/cmd/nerdctl/volume/volume_remove_linux_test.go b/cmd/nerdctl/volume/volume_remove_linux_test.go
new file mode 100644
index 00000000000..50bd936d6da
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_remove_linux_test.go
@@ -0,0 +1,230 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "errors"
+ "fmt"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+// TestVolumeRemove does test a large variety of volume remove situations, albeit none of them being
+// hard filesystem errors.
+// Behavior in such cases is largely unspecified, as there is no easy way to compare with Docker.
+// Anyhow, borked filesystem conditions is not something we should be expected to deal with in a smart way.
+func TestVolumeRemove(t *testing.T) {
+ testCase := nerdtest.Setup()
+
+ testCase.SubTests = []*test.Case{
+ {
+ Description: "arg missing should fail",
+ Command: test.Command("volume", "rm"),
+ Expected: test.Expects(1, []error{errors.New("requires at least 1 arg")}, nil),
+ },
+ {
+ Description: "invalid identifier should fail",
+ Command: test.Command("volume", "rm", "∞"),
+ Expected: test.Expects(1, []error{errdefs.ErrInvalidArgument}, nil),
+ },
+ {
+ Description: "non existent volume should fail",
+ Command: test.Command("volume", "rm", "doesnotexist"),
+ Expected: test.Expects(1, []error{errdefs.ErrNotFound}, nil),
+ },
+ {
+ Description: "busy volume should fail",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier())
+ helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()),
+ "--name", data.Identifier(), testutil.CommonImage)
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", data.Identifier())
+ },
+
+ Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil),
+ },
+ {
+ Description: "busy anonymous volume should fail",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("run", "-v", "/volume", "--name", data.Identifier(), testutil.CommonImage)
+ // Inspect the container and find the anonymous volume id
+ inspect := nerdtest.InspectContainer(helpers, data.Identifier())
+ var anonName string
+ for _, v := range inspect.Mounts {
+ if v.Destination == "/volume" {
+ anonName = v.Name
+ break
+ }
+ }
+ assert.Assert(t, anonName != "", "Failed to find anonymous volume id", inspect)
+ data.Set("anonName", anonName)
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Get("anonName"))
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ // Try to remove that anon volume
+ return helpers.Command("volume", "rm", data.Get("anonName"))
+ },
+
+ Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil),
+ },
+ {
+ Description: "freed volume should succeed",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier())
+ helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--name", data.Identifier(), testutil.CommonImage)
+ helpers.Ensure("rm", "-f", data.Identifier())
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", data.Identifier())
+ },
+
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ {
+ Description: "dangling volume should succeed",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier())
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", data.Identifier())
+ },
+
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ {
+ Description: "part success multi-remove",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier())
+ helpers.Ensure("volume", "create", data.Identifier("busy"))
+ helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage)
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy"))
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy"), data.Identifier())
+ },
+
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{
+ errdefs.ErrNotFound,
+ errdefs.ErrFailedPrecondition,
+ errdefs.ErrInvalidArgument,
+ },
+ Output: test.Equals(data.Identifier() + "\n"),
+ }
+ },
+ },
+ {
+ Description: "success multi-remove",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier("1"))
+ helpers.Ensure("volume", "create", data.Identifier("2"))
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("1"), data.Identifier("2"))
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", data.Identifier("1"), data.Identifier("2"))
+ },
+
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ Output: test.Equals(data.Identifier("1") + "\n" + data.Identifier("2") + "\n"),
+ }
+ },
+ },
+ {
+ Description: "failing multi-remove",
+
+ Setup: func(data test.Data, helpers test.Helpers) {
+ helpers.Ensure("volume", "create", data.Identifier("busy"))
+ helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier("busy")), "--name", data.Identifier(), testutil.CommonImage)
+ },
+
+ Cleanup: func(data test.Data, helpers test.Helpers) {
+ helpers.Anyhow("rm", "-f", data.Identifier())
+ helpers.Anyhow("volume", "rm", "-f", data.Identifier("busy"))
+ },
+
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("volume", "rm", "invalid∞", "nonexistent", data.Identifier("busy"))
+ },
+
+ Expected: test.Expects(1, []error{
+ errdefs.ErrNotFound,
+ errdefs.ErrFailedPrecondition,
+ errdefs.ErrInvalidArgument,
+ }, nil),
+ },
+ }
+
+ testCase.Run(t)
+}
diff --git a/cmd/nerdctl/volume/volume_test.go b/cmd/nerdctl/volume/volume_test.go
new file mode 100644
index 00000000000..980319ec60a
--- /dev/null
+++ b/cmd/nerdctl/volume/volume_test.go
@@ -0,0 +1,27 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package volume
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
+)
+
+func TestMain(m *testing.M) {
+ testutil.M(m)
+}
diff --git a/cmd/nerdctl/volume_create_test.go b/cmd/nerdctl/volume_create_test.go
deleted file mode 100644
index 742810b0223..00000000000
--- a/cmd/nerdctl/volume_create_test.go
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-// Test TestVolumeCreate for creating volume with given name.
-func TestVolumeCreate(t *testing.T) {
- base := testutil.NewBase(t)
- testVolume := testutil.Identifier(t)
-
- base.Cmd("volume", "create", testVolume).AssertOK()
- defer base.Cmd("volume", "rm", "-f", testVolume).Run()
-
- base.Cmd("volume", "list").AssertOutContains(testVolume)
-}
-
-// Test TestVolumeCreateTooManyArgs for creating volume with too many args.
-func TestVolumeCreateTooManyArgs(t *testing.T) {
- base := testutil.NewBase(t)
-
- base.Cmd("volume", "create", "too", "many").AssertFail()
-}
-
-// Test TestVolumeCreateWithLabels for creating volume with given labels.
-func TestVolumeCreateWithLabels(t *testing.T) {
- base := testutil.NewBase(t)
- testVolume := testutil.Identifier(t)
-
- base.Cmd("volume", "create", testVolume, "--label", "foo1=baz1", "--label", "foo2=baz2").AssertOK()
- defer base.Cmd("volume", "rm", "-f", testVolume).Run()
-
- inspect := base.InspectVolume(testVolume)
- inspectNerdctlLabels := *inspect.Labels
- expected := make(map[string]string, 2)
- expected["foo1"] = "baz1"
- expected["foo2"] = "baz2"
- assert.DeepEqual(base.T, expected, inspectNerdctlLabels)
-}
diff --git a/cmd/nerdctl/volume_inspect_test.go b/cmd/nerdctl/volume_inspect_test.go
deleted file mode 100644
index 6dc948b31cd..00000000000
--- a/cmd/nerdctl/volume_inspect_test.go
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "crypto/rand"
- "os"
- "path/filepath"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
- "gotest.tools/v3/assert"
-)
-
-func TestVolumeInspectContainsLabels(t *testing.T) {
- t.Parallel()
- testVolume := testutil.Identifier(t)
-
- base := testutil.NewBase(t)
- base.Cmd("volume", "create", "--label", "tag=testVolume", testVolume).AssertOK()
- defer base.Cmd("volume", "rm", "-f", testVolume).Run()
-
- inspect := base.InspectVolume(testVolume)
- inspectNerdctlLabels := (*inspect.Labels)
- expected := make(map[string]string, 1)
- expected["tag"] = "testVolume"
- assert.DeepEqual(base.T, expected, inspectNerdctlLabels)
-}
-
-func TestVolumeInspectSize(t *testing.T) {
- testutil.DockerIncompatible(t)
- t.Parallel()
- testVolume := testutil.Identifier(t)
- base := testutil.NewBase(t)
- base.Cmd("volume", "create", testVolume).AssertOK()
- defer base.Cmd("volume", "rm", "-f", testVolume).Run()
-
- var size int64 = 1028
- createFileWithSize(t, testVolume, size)
- volumeWithSize := base.InspectVolume(testVolume, []string{"--size"}...)
- assert.Equal(t, volumeWithSize.Size, size)
-}
-
-func createFileWithSize(t *testing.T, volume string, bytes int64) {
- base := testutil.NewBase(t)
- v := base.InspectVolume(volume)
- token := make([]byte, bytes)
- rand.Read(token)
- err := os.WriteFile(filepath.Join(v.Mountpoint, "test-file"), token, 0644)
- assert.NilError(t, err)
-}
diff --git a/cmd/nerdctl/volume_list_test.go b/cmd/nerdctl/volume_list_test.go
deleted file mode 100644
index 9a62dd102d4..00000000000
--- a/cmd/nerdctl/volume_list_test.go
+++ /dev/null
@@ -1,373 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "errors"
- "fmt"
- "strings"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/tabutil"
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestVolumeLs(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- testutil.DockerIncompatible(t)
-
- var vol1, vol2, vol3 = tID + "vol-1", tID + "vol-2", tID + "empty"
- base.Cmd("volume", "create", vol1).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol1).Run()
-
- base.Cmd("volume", "create", vol2).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol2).Run()
-
- base.Cmd("volume", "create", vol3).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol3).Run()
-
- createFileWithSize(t, vol1, 102400)
- createFileWithSize(t, vol2, 204800)
-
- base.Cmd("volume", "ls", "--size").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 4 {
- return errors.New("expected at least 4 lines")
- }
- volSizes := map[string]string{
- vol1: "100.0 KiB",
- vol2: "200.0 KiB",
- vol3: "0.0 B",
- }
-
- var numMatches = 0
- var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
- var err = tab.ParseHeader(lines[0])
- if err != nil {
- return err
- }
- for _, line := range lines {
- name, _ := tab.ReadRow(line, "VOLUME NAME")
- size, _ := tab.ReadRow(line, "SIZE")
- expectSize, ok := volSizes[name]
- if !ok {
- continue
- }
- if size != expectSize {
- return fmt.Errorf("expected size %s for volume %s, got %s", expectSize, name, size)
- }
- numMatches++
- }
- if len(volSizes) != numMatches {
- return fmt.Errorf("expected %d volumes, got: %d", len(volSizes), numMatches)
- }
- return nil
- })
-
-}
-
-func TestVolumeLsFilter(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
-
- var vol1, vol2, vol3, vol4 = tID + "vol-1", tID + "vol-2", tID + "vol-3", tID + "vol-4"
- var label1, label2, label3, label4 = tID + "=label-1", tID + "=label-2", tID + "=label-3", tID + "-group=label-4"
- base.Cmd("volume", "create", "--label="+label1, "--label="+label4, vol1).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol1).Run()
-
- base.Cmd("volume", "create", "--label="+label2, "--label="+label4, vol2).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol2).Run()
-
- base.Cmd("volume", "create", "--label="+label3, vol3).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol3).Run()
-
- base.Cmd("volume", "create", vol4).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol4).Run()
-
- base.Cmd("volume", "ls", "--quiet").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 4 {
- return errors.New("expected at least 4 lines")
- }
- volNames := map[string]struct{}{
- vol1: {},
- vol2: {},
- vol3: {},
- vol4: {},
- }
-
- var numMatches = 0
- for _, name := range lines {
- _, ok := volNames[name]
- if !ok {
- continue
- }
- numMatches++
- }
- if len(volNames) != numMatches {
- return fmt.Errorf("expected %d volumes, got: %d", len(volNames), numMatches)
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "label="+tID).AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 3 {
- return errors.New("expected at least 3 lines")
- }
- volNames := map[string]struct{}{
- vol1: {},
- vol2: {},
- vol3: {},
- }
-
- for _, name := range lines {
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "label="+label2).AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 1 {
- return errors.New("expected at least 1 lines")
- }
- volNames := map[string]struct{}{
- vol2: {},
- }
-
- for _, name := range lines {
- if name == "" {
- continue
- }
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "label="+tID+"=").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) > 0 {
- for _, name := range lines {
- if name != "" {
- return fmt.Errorf("unexpected volumes %d found", len(lines))
- }
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "label="+label1, "--filter", "label="+label2).AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) > 0 {
- for _, name := range lines {
- if name != "" {
- return fmt.Errorf("unexpected volumes %d found", len(lines))
- }
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "label="+tID, "--filter", "label="+label4).AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 2 {
- return errors.New("expected at least 2 lines")
- }
- volNames := map[string]struct{}{
- vol1: {},
- vol2: {},
- }
-
- for _, name := range lines {
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "name="+vol1).AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 1 {
- return errors.New("expected at least 1 lines")
- }
- volNames := map[string]struct{}{
- vol1: {},
- }
-
- for _, name := range lines {
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "name=vol-3").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 1 {
- return errors.New("expected at least 1 lines")
- }
- volNames := map[string]struct{}{
- vol3: {},
- }
-
- for _, name := range lines {
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--quiet", "--filter", "name=vol2", "--filter", "name=vol1").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) > 0 {
- for _, name := range lines {
- if name != "" {
- return fmt.Errorf("unexpected volumes %d found", len(lines))
- }
- }
- }
- return nil
- })
-}
-
-func TestVolumeLsFilterSize(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- testutil.DockerIncompatible(t)
-
- var vol1, vol2, vol3, vol4 = tID + "volsize-1", tID + "volsize-2", tID + "volsize-3", tID + "volsize-4"
- var label1, label2, label3, label4 = tID + "=label-1", tID + "=label-2", tID + "=label-3", tID + "-group=label-4"
- base.Cmd("volume", "create", "--label="+label1, "--label="+label4, vol1).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol1).Run()
-
- base.Cmd("volume", "create", "--label="+label2, "--label="+label4, vol2).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol2).Run()
-
- base.Cmd("volume", "create", "--label="+label3, vol3).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol3).Run()
-
- base.Cmd("volume", "create", vol4).AssertOK()
- defer base.Cmd("volume", "rm", "-f", vol4).Run()
-
- createFileWithSize(t, vol1, 409600)
- createFileWithSize(t, vol2, 1024000)
- createFileWithSize(t, vol3, 409600)
- createFileWithSize(t, vol4, 1024000)
-
- base.Cmd("volume", "ls", "--size", "--filter", "size=1024000").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 3 {
- return errors.New("expected at least 3 lines")
- }
-
- var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
- var err = tab.ParseHeader(lines[0])
- if err != nil {
- return err
- }
- volNames := map[string]struct{}{
- vol2: {},
- vol4: {},
- }
-
- for _, line := range lines {
- name, _ := tab.ReadRow(line, "VOLUME NAME")
- if name == "VOLUME NAME" {
- continue
- }
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--size", "--filter", "size>=1024000", "--filter", "size<=2048000").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 3 {
- return errors.New("expected at least 3 lines")
- }
-
- var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
- var err = tab.ParseHeader(lines[0])
- if err != nil {
- return err
- }
- volNames := map[string]struct{}{
- vol2: {},
- vol4: {},
- }
-
- for _, line := range lines {
- name, _ := tab.ReadRow(line, "VOLUME NAME")
- if name == "VOLUME NAME" {
- continue
- }
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-
- base.Cmd("volume", "ls", "--size", "--filter", "size>204800", "--filter", "size<1024000").AssertOutWithFunc(func(stdout string) error {
- var lines = strings.Split(strings.TrimSpace(stdout), "\n")
- if len(lines) < 3 {
- return errors.New("expected at least 3 lines")
- }
-
- var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE")
- var err = tab.ParseHeader(lines[0])
- if err != nil {
- return err
- }
- volNames := map[string]struct{}{
- vol1: {},
- vol3: {},
- }
-
- for _, line := range lines {
- name, _ := tab.ReadRow(line, "VOLUME NAME")
- if name == "VOLUME NAME" {
- continue
- }
- _, ok := volNames[name]
- if !ok {
- return fmt.Errorf("unexpected volume %s found", name)
- }
- }
- return nil
- })
-}
diff --git a/cmd/nerdctl/volume_prune_linux_test.go b/cmd/nerdctl/volume_prune_linux_test.go
deleted file mode 100644
index e9a53b064f0..00000000000
--- a/cmd/nerdctl/volume_prune_linux_test.go
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package main
-
-import (
- "fmt"
- "testing"
-
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestVolumePrune(t *testing.T) {
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
- base.Cmd("volume", "prune", "-f").Run()
-
- base.Cmd("volume", "create", tID+"-1").AssertOK()
- base.Cmd("volume", "create", tID+"-2").AssertOK()
-
- base.Cmd("run", "-v", fmt.Sprintf("%s:/volume", tID+"-1"), "--name", tID, testutil.CommonImage).AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
-
- base.Cmd("volume", "prune", "-f").AssertOutContains(tID + "-2")
- base.Cmd("volume", "ls").AssertOutContains(tID + "-1")
- base.Cmd("volume", "ls").AssertNoOut(tID + "-2")
-
- base.Cmd("rm", "-f", tID).AssertOK()
- base.Cmd("volume", "prune", "-f").AssertOK()
- base.Cmd("volume", "ls").AssertNoOut(tID + "-1")
-}
diff --git a/docs/cni.md b/docs/cni.md
index f404e6405fc..67313891604 100644
--- a/docs/cni.md
+++ b/docs/cni.md
@@ -61,10 +61,7 @@ Configuration of the default network `bridge` of Linux:
nerdctl >= 0.18 sets the `ingressPolicy` to `same-bridge` when `firewall` plugin >= 1.1.0 is installed.
This `ingressPolicy` replaces the CNI `isolation` plugin used in nerdctl <= 0.17.
-When the `isolation` plugin is found, nerdctl uses the `isolation` plugin instead of `ingressPolicy`.
-The `isolation` plugin has been deprecated, and a future version of `nerdctl` will solely support `ingressPolicy`.
-
-When neither of `firewall` plugin >= 1.1.0 or `isolation` plugin is found, nerdctl does not enable the bridge isolation.
+When `firewall` plugin >= 1.1.0 is not found, nerdctl does not enable the bridge isolation.
This means a container in `--net=foo` can connect to a container in `--net=bar`.
## macvlan/IPvlan networks
@@ -93,10 +90,42 @@ an easier way is to use DHCP to assign the IP:
Using `--driver ipvlan` can create `ipvlan` network, the default mode for IPvlan is `l2`.
+## DHCP host-name and other DHCP options
+
+Nerdctl automatically sets the DHCP host-name option to the hostname value of the container.
+
+Furthermore, on network creation, nerdctl supports the ability to set other DHCP options through `--ipam-options`.
+
+Currently, the following options are supported by the DHCP plugin:
+```
+dhcp-client-identifier
+subnet-mask
+routers
+user-class
+vendor-class-identifier
+```
+
+For example:
+```
+# nerdctl network create --driver macvlan \
+ --ipam-driver dhcp \
+ --ipam-opt 'vendor-class-identifier={"type": "provide", "value": "Hey! Its me!"}' \
+ my-dhcp-net
+```
+
## Custom networks
You can also customize your CNI network by providing configuration files.
-For example you have one configuration file(`/etc/cni/net.d/10-mynet.conf`)
+
+When rootful, the expected root location is `/etc/cni/net.d`.
+For rootless, the expected root location is `~/.config/cni/net.d/`
+
+Configuration files (like `10-mynet.conf`) can be placed either in the root location,
+or under a subfolder.
+If in the root location, this network will be available to all nerdctl namespaces.
+If placed in a subfolder, it will be available only to the identically named namespace.
+
+For example, you have one configuration file(`/etc/cni/net.d/10-mynet.conf`)
for `bridge` network:
```json
@@ -118,7 +147,7 @@ for `bridge` network:
```
This will configure a new CNI network with the name `mynet`, and you can use
-this network to create a container:
+this network to create a container in any namespace:
```console
# nerdctl run -it --net mynet --rm alpine ip addr show
diff --git a/docs/command-reference.md b/docs/command-reference.md
index 0e9c7c644d8..4284094d176 100644
--- a/docs/command-reference.md
+++ b/docs/command-reference.md
@@ -31,7 +31,9 @@ It does not necessarily mean that the corresponding features are missing in cont
- [:whale: nerdctl pause](#whale-nerdctl-pause)
- [:whale: nerdctl unpause](#whale-nerdctl-unpause)
- [:whale: nerdctl rename](#whale-nerdctl-rename)
+ - [:whale: nerdctl attach](#whale-nerdctl-attach)
- [:whale: nerdctl container prune](#whale-nerdctl-container-prune)
+ - [:whale: nerdctl diff](#whale-nerdctl-diff)
- [Build](#build)
- [:whale: nerdctl build](#whale-nerdctl-build)
- [:whale: nerdctl commit](#whale-nerdctl-commit)
@@ -109,6 +111,7 @@ It does not necessarily mean that the corresponding features are missing in cont
- [:whale: nerdctl compose pause](#whale-nerdctl-compose-pause)
- [:whale: nerdctl compose unpause](#whale-nerdctl-compose-unpause)
- [:whale: nerdctl compose config](#whale-nerdctl-compose-config)
+ - [:whale: nerdctl compose cp](#whale-nerdctl-compose-cp)
- [:whale: nerdctl compose kill](#whale-nerdctl-compose-kill)
- [:whale: nerdctl compose restart](#whale-nerdctl-compose-restart)
- [:whale: nerdctl compose rm](#whale-nerdctl-compose-rm)
@@ -131,12 +134,15 @@ Run a command in a new container.
Usage: `nerdctl run [OPTIONS] IMAGE [COMMAND] [ARG...]`
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IPFS. See [`ipfs.md`](./ipfs.md) for details.
+:nerd_face: `oci-archive://` prefix can be used for `IMAGE` to specify a local file system path to an OCI formatted tarball.
Basic flags:
+- :whale: `-a, --attach`: Attach STDIN, STDOUT, or STDERR
- :whale: :blue_square: `-i, --interactive`: Keep STDIN open even if not attached"
- :whale: :blue_square: `-t, --tty`: Allocate a pseudo-TTY
- :warning: WIP: currently `-t` conflicts with `-d`
+- :whale: `-sig-proxy`: Proxy received signals to the process (default true)
- :whale: :blue_square: `-d, --detach`: Run container in background and print container ID
- :whale: `--restart=(no|always|on-failure|unless-stopped)`: Restart policy to apply when a container exits
- Default: "no"
@@ -146,6 +152,7 @@ Basic flags:
- :whale: `--rm`: Automatically remove the container when it exits
- :whale: `--pull=(always|missing|never)`: Pull image before running
- Default: "missing"
+- :whale: `-q, --quiet`: Suppress the pull output
- :whale: `--pid=(host|container:)`: PID namespace to use
- :whale: `--uts=(host)` : UTS namespace to use
- :whale: `--stop-signal`: Signal to stop a container (default "SIGTERM")
@@ -169,9 +176,10 @@ Isolation flags:
Network flags:
-- :whale: `--net, --network=(bridge|host|none|container:|)`: Connect a container to a network.
+- :whale: `--net, --network=(bridge|host|none|container:|ns:|)`: Connect a container to a network.
- Default: "bridge"
- - 'container:': reuse another container's network stack, container has to be precreated.
+ - `container:`: reuse another container's network stack, container has to be precreated.
+ - :nerd_face: `ns:`: run inside an existing network namespace
- :nerd_face: Unlike Docker, this flag can be specified multiple times (`--net foo --net bar`)
- :whale: `-p, --publish`: Publish a container's port(s) to the host
- :whale: `--dns`: Set custom DNS servers
@@ -180,7 +188,8 @@ Network flags:
- :whale: `-h, --hostname`: Container host name
- :whale: `--add-host`: Add a custom host-to-IP mapping (host:ip). `ip` could be a special string `host-gateway`,
- which will be resolved to the `host-gateway-ip` in nerdctl.toml or global flag.
-- :whale: `--ip`: Specific static IP address(es) to use
+- :whale: `--ip`: Specific static IP address(es) to use. Note that unlike docker, nerdctl allows specifying it with the default bridge network.
+- :whale: `--ip6`: Specific static IP6 address(es) to use. Should be used with user networks
- :whale: `--mac-address`: Specific MAC address to use. Be aware that it does not
check if manually specified MAC addresses are unique. Supports network
type `bridge` and `macvlan`
@@ -224,10 +233,20 @@ Security flags:
- :whale: `--security-opt seccomp=`: specify custom seccomp profile
- :whale: `--security-opt apparmor=`: specify custom AppArmor profile
- :whale: `--security-opt no-new-privileges`: disallow privilege escalation, e.g., setuid and file capabilities
+- :whale: `--security-opt systempaths=unconfined`: Turn off confinement for system paths (masked paths, read-only paths) for the container
- :nerd_face: `--security-opt privileged-without-host-devices`: Don't pass host devices to privileged containers
- :whale: `--cap-add=`: Add Linux capabilities
- :whale: `--cap-drop=`: Drop Linux capabilities
- :whale: `--privileged`: Give extended privileges to this container
+- :nerd_face: `--systemd=(true|false|always)`: Enable systemd compatibility (default: false).
+ - Default: "false"
+ - true: Enable systemd compatibility is enabled if the entrypoint executable matches one of the following paths:
+ - `/sbin/init`
+ - `/usr/sbin/init`
+ - `/usr/local/sbin/init`
+ - always: Always enable systemd compatibility
+
+Corresponds to Podman CLI.
Runtime flags:
@@ -251,12 +270,12 @@ Volume flags:
consisting of a `=` tuple.
e.g., `-- mount type=bind,source=/src,target=/app,bind-propagation=shared`.
- :whale: `type`: Current supported mount types are `bind`, `volume`, `tmpfs`.
- The defaul type will be set to `volume` if not specified.
- i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volum,src=vol-1,dst=/app,readonly`
+ The default type will be set to `volume` if not specified.
+ i.e., `--mount src=vol-1,dst=/app,readonly` equals `--mount type=volume,src=vol-1,dst=/app,readonly`
- Common Options:
- :whale: `src`, `source`: Mount source spec for bind and volume. Mandatory for bind.
- :whale: `dst`, `destination`, `target`: Mount destination spec.
- - :whale: `readonly`, `ro`, `rw`, `rro`: Filesystem permissinos.
+ - :whale: `readonly`, `ro`, `rw`, `rro`: Filesystem permissions.
- Options specific to `bind`:
- :whale: `bind-propagation`: `shared`, `slave`, `private`, `rshared`, `rslave`, or `rprivate`(default).
- :whale: `bind-nonrecursive`: `true` or `false`(default). If set to true, submounts are not recursively bind-mounted. This option is useful for readonly bind mount.
@@ -267,6 +286,7 @@ Volume flags:
Defaults to `1777` or world-writable.
- Options specific to `volume`:
- unimplemented options: `volume-nocopy`, `volume-label`, `volume-driver`, `volume-opt`
+- :whale: `--volumes-from`: Mount volumes from the specified container(s), e.g. "--volumes-from my-container".
Rootfs flags:
@@ -284,8 +304,9 @@ Env flags:
Metadata flags:
- :whale: :blue_square: `--name`: Assign a name to the container
-- :whale: :blue_square: `-l, --label`: Set meta data on a container
+- :whale: :blue_square: `-l, --label`: Set meta data on a container (Not passed through the OCI runtime since nerdctl v2.0, with an exception for `nerdctl/bypass4netns`)
- :whale: :blue_square: `--label-file`: Read in a line delimited file of labels
+- :whale: :blue_square: `--annotation`: Add an annotation to the container (passed through to the OCI runtime)
- :whale: :blue_square: `--cidfile`: Write the container ID to the file
- :nerd_face: `--pidfile`: file path to write the task's pid. The CLI syntax conforms to Podman convention.
@@ -346,7 +367,7 @@ Logging flags:
Shared memory flags:
-- :whale: `--ipc`: IPC namespace to use
+- :whale: `--ipc=(host|private|shareable|container:)`: IPC namespace to use and mount `/dev/shm`. Default: "private". Only implemented on Linux.
- :whale: `--shm-size`: Size of `/dev/shm`
GPU flags:
@@ -357,6 +378,26 @@ Ulimit flags:
- :whale: `--ulimit`: Set ulimit
+--ulimit can be used to restrict the following types of resources.
+
+| type | describe| value range |
+|----|----|----|
+| core | limits the core file size (KB)| A 64-bit integer (INT64), with no units. It can be 0, negative, where -1 represents UNLIMITED (i.e., no limit is applied), and any other negative values will be forcibly converted to a large positive integer.|
+| cpu | max CPU time (MIN)| same as above|
+| data |max data size (KB) | same as above|
+| fsize | maximum filesize (KB)| same as above|
+| locks | max number of file locks the user can hold | same as above|
+| memlock | max locked-in-memory address space (KB) | same as above|
+| msgqueue | max memory used by POSIX message queues (bytes)| same as above|
+| nice | nice priority | same as above |
+| nproc | max number of processes | same as above|
+| rss | max resident set size (KB)| same as above|
+| rtprio | max realtime priority| same as above|
+| rttime | realtime timeout | same as above|
+| sigpending | max number of pending signals| same as above|
+| stack | max stack size (KB) | same as above|
+| nofile | max number of open file descriptors| A 64-bit integer (int64), with no units. It cannot be negative; negative values will be forcibly converted to a large number, and an "Operation not permitted" error will occur during setting|
+
Verify flags:
- :nerd_face: `--verify`: Verify the image (none|cosign|notation). See [`./cosign.md`](./cosign.md) and [`./notation.md`](./notation.md) for details.
@@ -371,10 +412,10 @@ IPFS flags:
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)
Unimplemented `docker run` flags:
- `--attach`, `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
- `--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--ip6`, `--isolation`, `--no-healthcheck`,
- `--link*`, `--mac-address`, `--publish-all`, `--sig-proxy`, `--storage-opt`,
- `--userns`, `--volume-driver`, `--volumes-from`
+ `--blkio-weight-device`, `--cpu-rt-*`, `--device-*`,
+ `--disable-content-trust`, `--domainname`, `--expose`, `--health-*`, `--isolation`, `--no-healthcheck`,
+ `--link*`, `--publish-all`, `--storage-opt`,
+ `--userns`, `--volume-driver`
### :whale: :blue_square: nerdctl exec
@@ -403,6 +444,7 @@ Create a new container.
Usage: `nerdctl create [OPTIONS] IMAGE [COMMAND] [ARG...]`
:nerd_face: `ipfs://` prefix can be used for `IMAGE` to pull it from IPFS. See [`ipfs.md`](./ipfs.md) for details.
+:nerd_face: `oci-archive://` prefix can be used for `IMAGE` to specify a local file system path to an OCI formatted tarball.
The `nerdctl create` command similar to `nerdctl run -d` except the container is never started. You can then use the `nerdctl start ` command to start the container at any point.
@@ -443,7 +485,7 @@ Flags:
- :nerd_face: `--format=json`: Alias of `--format='{{json .}}'`
- :whale: `-n, --last`: Show n last created containers (includes all states)
- :whale: `-l, --latest`: Show the latest created container (includes all states)
-- :whale: `-f, --filter`: Filter containers based on given conditions
+- :whale: `-f, --filter`: Filter containers based on given conditions. When specifying the condition 'status', it filters all containers
- :whale: `--filter id=`: Container's ID. Both full ID and
truncated ID are supported
- :whale: `--filter name=`: Container's name
@@ -453,7 +495,7 @@ Flags:
`--all`
- :whale: `--filter status=`: One of `created, running, paused,
stopped, exited, pausing, unknown`. Note that `restarting, removing, dead` are
- not supported and will be ignored
+ not supported and will be ignored. When specifying this condition, it filters all containers.
- :whale: `--filter before/since=`: Filter containers created before
or after a given ID or name
- :whale: `--filter volume=`: Filter by a given mounted volume or bind
@@ -479,6 +521,7 @@ Flags:
- :nerd_face: `--mode=(dockercompat|native)`: Inspection mode. "native" produces more information.
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
- :whale: `--type`: Return JSON for specified type
+- :whale: `--size`: Display total file sizes if the type is container
Unimplemented `docker inspect` flags: `--size`
@@ -608,6 +651,30 @@ Rename a container.
Usage: `nerdctl rename CONTAINER NEW_NAME`
+### :whale: nerdctl attach
+
+Attach stdin, stdout, and stderr to a running container. For example:
+
+1. `nerdctl run -it --name test busybox` to start a container with a pty
+2. `ctrl-p ctrl-q` to detach from the container
+3. `nerdctl attach test` to attach to the container
+
+Caveats:
+
+- Currently only one attach session is allowed. When the second session tries to attach, currently no error will be returned from nerdctl.
+ However, since behind the scenes, there's only one FIFO for stdin, stdout, and stderr respectively,
+ if there are multiple sessions, all the sessions will be reading from and writing to the same 3 FIFOs, which will result in mixed input and partial output.
+- Until dual logging (issue #1946) is implemented,
+ a container that is spun up by either `nerdctl run -d` or `nerdctl start` (without `--attach`) cannot be attached to.
+
+Usage: `nerdctl attach CONTAINER`
+
+Flags:
+
+- :whale: `--detach-keys`: Override the default detach keys
+
+Unimplemented `docker attach` flags: `--no-stdin`, `--sig-proxy`
+
### :whale: nerdctl container prune
Remove all stopped containers.
@@ -620,6 +687,12 @@ Flags:
Unimplemented `docker container prune` flags: `--filter`
+### :whale: nerdctl diff
+
+Inspect changes to files or directories on a container's filesystem
+
+Usage: `nerdctl diff CONTAINER`
+
## Build
### :whale: nerdctl build
@@ -645,17 +718,25 @@ Flags:
- :whale: `type=tar[,dest=path/to/output.tar]`: Raw tar ball
- :whale: `type=image,name=example.com/image,push=true`: Push to a registry (see [`buildctl build`](https://github.com/moby/buildkit/tree/v0.9.0#imageregistry) documentation)
- :whale: `--progress=(auto|plain|tty)`: Set type of progress output (auto, plain, tty). Use plain to show container output
+- :whale: `--provenance`: Shorthand for \"--attest=type=provenance\", see [`buildx_build.md`](https://github.com/docker/buildx/blob/v0.12.1/docs/reference/buildx_build.md#provenance) documentation
+- :whale: `--pull=(true|false)`: On true, always attempt to pull latest image version from remote. Default uses buildkit's default.
- :whale: `--secret`: Secret file to expose to the build: id=mysecret,src=/local/secret
+- :whale: `--allow`: Allow extra privileged entitlement, e.g. network.host, security.insecure (It’s required to configure the buildkitd to enable the feature, see [`buildkitd.toml`](https://github.com/moby/buildkit/blob/master/docs/buildkitd.toml.md) documentation)
+- :whale: `--attest`: Attestation parameters (format: "type=sbom,generator=image"), see [`buildx_build.md`](https://github.com/docker/buildx/blob/v0.12.1/docs/reference/buildx_build.md#attest) documentation
- :whale: `--ssh`: SSH agent socket or keys to expose to the build (format: `default|[=|[,]]`)
- :whale: `-q, --quiet`: Suppress the build output and print image ID on success
+- :whale: `--sbom`: Shorthand for \"--attest=type=sbom\", see [`buildx_build.md`](https://github.com/docker/buildx/blob/v0.12.1/docs/reference/buildx_build.md#sbom) documentation
- :whale: `--cache-from=CACHE`: External cache sources (eg. user/app:cache, type=local,src=path/to/dir) (compatible with `docker buildx build`)
- :whale: `--cache-to=CACHE`: Cache export destinations (eg. user/app:cache, type=local,dest=path/to/dir) (compatible with `docker buildx build`)
- :whale: `--platform=(amd64|arm64|...)`: Set target platform for build (compatible with `docker buildx build`)
- :whale: `--iidfile=FILE`: Write the image ID to the file
- :nerd_face: `--ipfs`: Build image with pulling base images from IPFS. See [`ipfs.md`](./ipfs.md) for details.
- :whale: `--label`: Set metadata for an image
+- :whale: `--network=(default|host|none)`: Set the networking mode for the RUN instructions during build.(compatible with `buildctl build`)
+- :whale: `--build-context`: Set additional contexts for build (e.g. dir2=/path/to/dir2, myorg/myapp=docker-image://path/to/myorg/myapp)
+- :whale: `--add-host`: Add a custom host-to-IP mapping (format: `host:ip`)
-Unimplemented `docker build` flags: `--add-host`, `--network`, `--squash`
+Unimplemented `docker build` flags: `--squash`
### :whale: nerdctl commit
@@ -691,7 +772,7 @@ Flags:
- :nerd_face: `--format=wide`: Wide table
- :nerd_face: `--format=json`: Alias of `--format='{{json .}}'`
- :whale: `--digests`: Show digests (compatible with Docker, unlike ID)
-- :whale: `-f, --filter`: Filter the images. For now, only 'before=' and 'since=' is supported.
+- :whale: `-f, --filter`: Filter the images.
- :whale: `--filter=before=`: Images created before given image (exclusive)
- :whale: `--filter=since=`: Images created after given image (exclusive)
- :whale: `--filter=label=`: Matches images based on the presence of a label alone or a label and a value
@@ -721,6 +802,7 @@ Flags:
- :nerd_face: `--cosign-certificate-oidc-issuer`: The OIDC issuer expected in a valid Fulcio certificate for --verify=cosign,, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows
- :nerd_face: `--cosign-certificate-oidc-issuer-regexp`: A regular expression alternative to --certificate-oidc-issuer for --verify=cosign,. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)
+- :nerd_face: `--soci-index-digest`: Specify a particular index digest for SOCI. If left empty, SOCI will automatically use the index determined by the selection policy.
Unimplemented `docker pull` flags: `--all-tags`, `--disable-content-trust` (default true)
@@ -741,8 +823,11 @@ Flags:
- :nerd_face: `--notation-key-name`: Signing key name for a key previously added to notation's key list for `--sign=notation`
- :nerd_face: `--allow-nondistributable-artifacts`: Allow pushing images with non-distributable blobs
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)
+- :whale: `-q, --quiet`: Suppress verbose output
+- :nerd_face: `--soci-span-size`: Span size in bytes that soci index uses to segment layer data. Default is 4 MiB.
+- :nerd_face: `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
-Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true), `--quiet`
+Unimplemented `docker push` flags: `--all-tags`, `--disable-content-trust` (default true)
### :whale: nerdctl load
@@ -755,11 +840,10 @@ Usage: `nerdctl load [OPTIONS]`
Flags:
- :whale: `-i, --input`: Read from tar archive file, instead of STDIN
+- :whale: `-q, --quiet`: Suppress the load output
- :nerd_face: `--platform=(amd64|arm64|...)`: Import content for a specific platform
- :nerd_face: `--all-platforms`: Import content for all platforms
-Unimplemented `docker load` flags: `--quiet`
-
### :whale: nerdctl save
Save one or more images to a tar archive (streamed to STDOUT by default)
@@ -816,6 +900,7 @@ Flags:
- :whale: `--no-trunc`: Don't truncate output
- :whale: `-q, --quiet`: Only display snapshots IDs
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
+- :whale: `-H, --human`: Print sizes and dates in human readable format (default true)
### :whale: nerdctl image prune
@@ -826,10 +911,11 @@ Usage: `nerdctl image prune [OPTIONS]`
Flags:
- :whale: `-a, --all`: Remove all unused images, not just dangling ones
+- :whale: `-f, --filter`: Filter the images.
+ - :whale: `--filter=until=`: Images created before given date formatted timestamps or Go duration strings. Currently does not support Unix timestamps.
+ - :whale: `--filter=label=`: Matches images based on the presence of a label alone or a label and a value
- :whale: `-f, --force`: Do not prompt for confirmation
-Unimplemented `docker image prune` flags: `--filter`
-
### :nerd_face: nerdctl image convert
Convert an image format.
@@ -847,6 +933,8 @@ Flags:
- `--estargz-min-chunk-size=` : The minimal number of bytes of data must be written in one gzip stream (requires stargz-snapshotter >= v0.13.0). Useful for creating a smaller eStargz image (refer to [`./stargz.md`](./stargz.md) for details).
- `--estargz-external-toc` : Separate TOC JSON into another image (called \"TOC image\"). The name of TOC image is the original + \"-esgztoc\" suffix. Both eStargz and the TOC image should be pushed to the same registry. (requires stargz-snapshotter >= v0.13.0) Useful for creating a smaller eStargz image (refer to [`./stargz.md`](./stargz.md) for details). :warning: This flag is experimental and subject to change.
- `--estargz-keep-diff-id`: Convert to esgz without changing diffID (cannot be used in conjunction with '--estargz-record-in'. must be specified with '--estargz-external-toc')
+- `--zstd` : Use zstd compression instead of gzip. Should be used in conjunction with '--oci'
+- `--zstd-compression-level=` : zstd compression level (default: 3)
- `--zstdchunked` : Use zstd compression instead of gzip (a.k.a zstd:chunked). Should be used in conjunction with '--oci'
- `--zstdchunked-record-in=` : read `ctr-remote optimize --record-out=` record file. :warning: This flag is experimental and subject to change.
- `--zstdchunked-compression-level=`: zstd:chunked compression level (default: 3)
@@ -959,8 +1047,9 @@ Flags:
- :whale: `--gateway`: Gateway for the master subnet
- :whale: `--ip-range`: Allocate container ip from a sub-range
- :whale: `--label`: Set metadata on a network
+- :whale: `--ipv6`: Enable IPv6. Should be used with a valid subnet.
-Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--ipv6`, `--scope`
+Unimplemented `docker network create` flags: `--attachable`, `--aux-address`, `--config-from`, `--config-only`, `--ingress`, `--internal`, `--scope`
### :whale: nerdctl network ls
@@ -977,7 +1066,7 @@ Flags:
- :nerd_face: `--format=wide`: Alias of `--format=table`
- :nerd_face: `--format=json`: Alias of `--format='{{json .}}'`
-Unimplemented `docker network ls` flags: `--filter`, `--no-trunc`
+Unimplemented `docker network ls` flags: `--no-trunc`
### :whale: nerdctl network inspect
@@ -1127,7 +1216,7 @@ Flags:
### :nerd_face: :blue_square: nerdctl namespace update
-Udapte labels for a namespace.
+Update labels for a namespace.
Usage: `nerdctl namespace update NAMESPACE`
@@ -1179,6 +1268,10 @@ Usage: `nerdctl builder prune`
Flags:
- :nerd_face: `--buildkit-host=`: BuildKit address
+- :whale: `--all`: Remove all unused build cache, not just dangling ones
+- :whale: `--force`: Do not prompt for confirmation
+
+Unimplemented `docker builder prune` flags: `--filter`, `--keep-storage`
### :nerd_face: nerdctl builder debug
@@ -1188,6 +1281,8 @@ This is an [experimental](./experimental.md) feature.
:warning: This command currently doesn't use the host's `buildkitd` daemon but uses the patched version of BuildKit provided by buildg. This should be fixed in the future.
+:warning: This command is currently incompatible with `docker buildx debug`.
+
Usage: `nerdctl builder debug PATH`
Flags:
@@ -1197,8 +1292,6 @@ Flags:
- :nerd_face: `--target`: Set the target build stage to build
- :nerd_face: `--build-arg`: Set build-time variables
-Unimplemented `docker builder prune` flags: `--all`, `--filter`, `--force`, `--keep-storage`
-
## System
### :whale: nerdctl events
@@ -1212,8 +1305,10 @@ Usage: `nerdctl events [OPTIONS]`
Flags:
- :whale: `--format`: Format the output using the given Go template, e.g, `{{json .}}`
+- :whale: `-f, --filter`: Filter containers based on given conditions
+ - :whale: `--filter event=`: Event's status. Start is the only supported status.
-Unimplemented `docker events` flags: `--filter`, `--since`, `--until`
+Unimplemented `docker events` flags: `--since`, `--until`
### :whale: nerdctl info
@@ -1317,6 +1412,7 @@ Flags:
- :whale: `-p, --project-name`: Specify an alternate project name
- :nerd_face: `--ipfs-address`: Multiaddr of IPFS API (default uses `$IPFS_PATH` env variable if defined or local directory `~/.ipfs`)
- :whale: `--profile: Specify a profile to enable
+- :whale: `--env-file` : Specify an alternate environment file
### :whale: nerdctl compose up
@@ -1326,7 +1422,8 @@ Usage: `nerdctl compose up [OPTIONS] [SERVICE...]`
Flags:
-- :whale: `-d, --detach`: Detached mode: Run containers in the background
+- :whale: `--abort-on-container-exit`: Stops all containers if any container was stopped. Incompatible with `-d`.
+- :whale: `-d, --detach`: Detached mode: Run containers in the background. Incompatible with `--abort-on-container-exit`.
- :whale: `--no-build`: Don't build an image, even if it's missing.
- :whale: `--no-color`: Produce monochrome output
- :whale: `--no-log-prefix`: Don't print prefix in logs
@@ -1335,15 +1432,18 @@ Flags:
- :whale: `--quiet-pull`: Pull without printing progress information
- :whale: `--scale`: Scale SERVICE to NUM instances. Overrides the `scale` setting in the Compose file if present.
- :whale: `--remove-orphans`: Remove containers for services not defined in the Compose file
+- :whale: `--force-recreate`: force Compose to stop and recreate all containers
+- :whale: `--no-recreate`: force Compose to reuse existing containers
+- :whale: `--pull`: Pull image before running ("always"|"missing"|"never")
-Unimplemented `docker-compose up` (V1) flags: `--no-deps`, `--force-recreate`, `--always-recreate-deps`, `--no-recreate`,
+Unimplemented `docker-compose up` (V1) flags: `--no-deps`, `--always-recreate-deps`,
`--no-start`, `--abort-on-container-exit`, `--attach-dependencies`, `--timeout`, `--renew-anon-volumes`, `--exit-code-from`
Unimplemented `docker compose up` (V2) flags: `--environment`
### :whale: nerdctl compose logs
-Create and start containers
+Show logs of running containers
Usage: `nerdctl compose logs [OPTIONS] [SERVICE...]`
@@ -1399,11 +1499,10 @@ Flags:
- :whale: `-i, --interactive`: Keep STDIN open even if not attached (default true)
- :whale: `--privileged`: Give extended privileges to the command
- :whale: `-t, --tty`: Allocate a pseudo-TTY
+- :whale: `-T, --no-TTY`: Disable pseudo-TTY allocation. By default nerdctl compose exec allocates a TTY.
- :whale: `-u, --user`: Username or UID (format: `[:]`)
- :whale: `-w, --workdir`: Working directory inside the container
-Unimplemented `docker-compose exec` (V2) flags: `-T, --no-TTY`
-
### :whale: nerdctl compose down
Remove containers and associated resources
@@ -1460,9 +1559,17 @@ List containers of services
Usage: `nerdctl compose ps [OPTIONS] [SERVICE...]`
-Unimplemented `docker-compose ps` (V1) flags: `--quiet`, `--services`, `--filter`, `--all`
-
-Unimplemented `docker compose ps` (V2) flags: `--status`
+- :whale: `-a, --all`: Show all containers (default shows just running)
+- :whale: `-q, --quiet`: Only display container IDs
+- :whale: `--format`: Format the output
+ - :whale: `--format=table` (default): Table
+ - :whale: `--format=json'`: JSON
+- :whale: `-f, --filter`: Filter containers based on given conditions
+ - :whale: `--filter status=`: One of `created, running, paused,
+ restarting, exited, pausing, unknown`. Note that `removing, dead` are
+ not supported and will be ignored
+- :whale: `--services`: Print the service names, one per line
+- :whale: `--status`: Filter containers by status. Values: [paused | restarting | running | created | exited | pausing | unknown]
### :whale: nerdctl compose pull
@@ -1513,6 +1620,23 @@ Unimplemented `docker-compose config` (V1) flags: `--resolve-image-digests`, `--
Unimplemented `docker compose config` (V2) flags: `--resolve-image-digests`, `--no-interpolate`, `--format`, `--output`, `--profiles`
+### :whale: nerdctl compose cp
+
+Copy files/folders between a service container and the local filesystem
+
+Usage:
+```
+nerdctl compose cp [OPTIONS] SERVICE:SRC_PATH DEST_PATH|-
+nerdctl compose cp [OPTIONS] SRC_PATH|- SERVICE:DEST_PATH [flags]
+```
+
+Flags:
+- :whale: `--dry-run`: Execute command in dry run mode
+- :whale: `-L, --follow-link`: Always follow symbol link in SRC_PATH
+- :whale: `--index int`: index of the container if service has multiple replicas
+
+Unimplemented `docker compose cp` flags: `--archive`
+
### :whale: nerdctl compose kill
Force stop service containers
@@ -1618,7 +1742,6 @@ See [`./config.md`](./config.md).
Container management:
-- `docker attach`
- `docker diff`
- `docker checkpoint *`
diff --git a/docs/compose.md b/docs/compose.md
index ac8cf696383..a07c91a5207 100644
--- a/docs/compose.md
+++ b/docs/compose.md
@@ -29,7 +29,6 @@ which was derived from [Docker Compose file version 3 specification](https://doc
- `services..deploy.placement`
- `services..deploy.endpoint_mode`
- `services..healthcheck`
-- `services..profiles`
- `services..stop_grace_period`
- `services..stop_signal`
- `configs..external`
diff --git a/docs/config.md b/docs/config.md
index 2aca5dbce2c..1f0b4cd15e5 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -45,6 +45,8 @@ experimental = true
| `hosts_dir` | `--hosts-dir` | | `certs.d` directory | Since 0.16.0 |
| `experimental` | `--experimental` | `NERDCTL_EXPERIMENTAL` | Enable [experimental features](experimental.md) | Since 0.22.3 |
| `host_gateway_ip` | `--host-gateway-ip` | `NERDCTL_HOST_GATEWAY_IP` | IP address that the special 'host-gateway' string in --add-host resolves to. Defaults to the IP address of the host. It has no effect without setting --add-host | Since 1.3.0 |
+| `bridge_ip` | `--bridge-ip` | `NERDCTL_BRIDGE_IP` | IP address for the default nerdctl bridge network, e.g., 10.1.100.1/24 | Since 2.0.1 |
+| `kube-hide-dupe` | `--kube-hide-dupe` | | Deduplicate images for Kubernetes with namespace k8s.io, no more redundant ones are displayed | Since 2.0.3 |
The properties are parsed in the following precedence:
1. CLI flag
diff --git a/docs/cvmfs.md b/docs/cvmfs.md
new file mode 100644
index 00000000000..7e3c812a62a
--- /dev/null
+++ b/docs/cvmfs.md
@@ -0,0 +1,89 @@
+# Lazy-pulling using CernVM-FS Snapshotter
+
+CernVM-FS Snapshotter is a containerd snapshotter plugin. It is a specialized component responsible for assembling
+all the layers of container images into a stacked file system that containerd can use. The snapshotter takes as input the list
+of required layers and outputs a directory containing the final file system. It is also responsible to clean up the output
+directory when containers using it are stopped.
+
+See the official [documentation](https://cvmfs.readthedocs.io/en/latest/cpt-containers.html#how-to-use-the-cernvm-fs-snapshotter) to learn further information.
+
+## Prerequisites
+
+- Install containerd remote snapshotter plugin (`cvmfs-snapshotter`) from [here](https://github.com/cvmfs/cvmfs/tree/devel/snapshotter).
+
+- Add the following to `/etc/containerd/config.toml`:
+```toml
+# Ask containerd to use this particular snapshotter
+[plugins."io.containerd.grpc.v1.cri".containerd]
+ snapshotter = "cvmfs-snapshotter"
+ disable_snapshot_annotations = false
+
+# Set the communication endpoint between containerd and the snapshotter
+[proxy_plugins]
+ [proxy_plugins.cvmfs]
+ type = "snapshot"
+ address = "/run/containerd-cvmfs-grpc/containerd-cvmfs-grpc.sock"
+```
+- The default CernVM-FS repository hosting the flat root filesystems of the container images is `unpacked.cern.ch`.
+ The container images are unpacked into the CernVM-FS repository by the [DUCC](https://cvmfs.readthedocs.io/en/latest/cpt-ducc.html)
+ (Daemon that Unpacks Container Images into CernVM-FS) tool.
+ You can change the repository adding the following line to `/etc/containerd-cvmfs-grpc/config.toml`:
+```toml
+repository = "myrepo.mydomain"
+```
+- Launch `containerd` and `cvmfs-snapshotter`:
+```console
+$ systemctl start containerd cvmfs-snapshotter
+```
+
+## Enable CernVM-FS Snapshotter for `nerdctl run` and `nerdctl pull`
+
+| :zap: Requirement | nerdctl >= 1.6.3 |
+| ----------------- | ---------------- |
+
+- Run `nerdctl` with `--snapshotter cvmfs-snapshotter` as in the example below:
+```console
+$ nerdctl run -it --rm --snapshotter cvmfs-snapshotter clelange/cms-higgs-4l-full:latest
+```
+
+- You can also only pull the image with CernVM-FS Snapshotter without running the container:
+```console
+$ nerdctl pull --snapshotter cvmfs-snapshotter clelange/cms-higgs-4l-full:latest
+```
+
+The speedup for pulling this 9 GB (4.3 GB compressed) image is shown below:
+- #### with the snapshotter:
+```console
+$ nerdctl --snapshotter cvmfs-snapshotter pull clelange/cms-higgs-4l-full:latest
+docker.io/clelange/cms-higgs-4l-full:latest: resolved |++++++++++++++++++++++++++++++++++++++|
+manifest-sha256:b8acbe80629dd28d213c03cf1ffd3d46d39e573f54215a281fabce7494b3d546: done |++++++++++++++++++++++++++++++++++++++|
+config-sha256:89ef54b6c4fbbedeeeb29b1df2b9916b6d157c87cf1878ea882bff86a3093b5c: done |++++++++++++++++++++++++++++++++++++++|
+elapsed: 4.7 s total: 19.8 K (4.2 KiB/s)
+
+$ nerdctl images
+REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
+clelange/cms-higgs-4l-full latest b8acbe80629d 20 seconds ago linux/amd64 0.0 B 4.3 GiB
+```
+- #### without the snapshotter:
+```console
+$ nerdctl pull clelange/cms-higgs-4l-full:latest
+docker.io/clelange/cms-higgs-4l-full:latest: resolved |++++++++++++++++++++++++++++++++++++++|
+manifest-sha256:b8acbe80629dd28d213c03cf1ffd3d46d39e573f54215a281fabce7494b3d546: exists |++++++++++++++++++++++++++++++++++++++|
+config-sha256:89ef54b6c4fbbedeeeb29b1df2b9916b6d157c87cf1878ea882bff86a3093b5c: exists |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:e8114d4b0d10b33aaaa4fbc3c6da22bbbcf6f0ef0291170837e7c8092b73840a: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:a3eda0944a81e87c7a44b117b1c2e707bc8d18e9b7b478e21698c11ce3e8b819: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:8f3160776e8e8736ea9e3f6c870d14cd104143824bbcabe78697315daca0b9ad: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:22a5c05baa9db0aa7bba56ffdb2dd21246b9cf3ce938fc6d7bf20e92a067060e: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:bfcf9d498f92b72426c9d5b73663504d87249d6783c6b58d71fbafc275349ab9: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:0563e1549926b9c8beac62407bc6a420fa35bcf6f9844e5d8beeb9165325a872: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:6fff5fd7fb4eeb79a1399d9508614a84191d05e53f094832062d689245599640: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:25c39bfa66e1157415236703abc512d06cc1db31bd00fe8c3030c6d6d249dc4e: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:3cc0a0eb55eb3fb7ef0760c6bf1e567dfc56933ba5f11b5415f89228af751b72: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:a8850244786303e508b94bb31c8569310765e678c9c73bf1199310729209b803: done |++++++++++++++++++++++++++++++++++++++|
+layer-sha256:32cdf5fc12485ac061347eb8b5c3b4a28505ce8564a7f3f83ac4241f03911176: done |++++++++++++++++++++++++++++++++++++++|
+elapsed: 181.8s total: 4.3 Gi (24.2 MiB/s)
+
+$ nerdctl images
+REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
+clelange/cms-higgs-4l-full latest b8acbe80629d 4 minutes ago linux/amd64 9.0 GiB 4.3 GiB
+```
diff --git a/docs/dev/store.md b/docs/dev/store.md
new file mode 100644
index 00000000000..c0954fb0063
--- /dev/null
+++ b/docs/dev/store.md
@@ -0,0 +1,163 @@
+# About `pkg/store`
+
+## TL;DR
+
+You _may_ want to read this if you are developing something in nerdctl that would involve storing persistent information.
+
+If there is a "store" already in the codebase (eg: volumestore, namestore, etc) that does provide the methods that you need,
+you are fine and should just stick to that.
+
+On the other hand, if you are curious, or if what you want to write is "new", then _you should_ have a look at this document:
+it does provide extended information about how we manage persistent data storage, especially with regard to concurrency
+and atomicity.
+
+## Motivation
+
+The core of nerdctl aims at keeping its dependencies as lightweight as possible.
+For that reason, nerdctl does not use a database to store persistent information, but instead uses the filesystem,
+under a variety of directories.
+
+That "information" is typically volumes metadata, containers lifecycle info, the "name store" (which does ensure no two
+containers can be named the same), etc.
+
+However, storing data on the filesystem in a reliable way comes with challenges:
+- incomplete writes may happen (because of a system restart, or an application crash), leaving important structured files
+in a broken state
+- concurrent writes, or reading while writing would obviously be a problem as well, be it accross goroutines, or between
+concurrent executions of the nerdctl binary, or embedded in a third-party application that does concurrently access resources
+
+The `pkg/store` package does provide a "storage" abstraction that takes care of these issues, generally providing
+guarantees that concurrent operations can be performed safely, and that writes are "atomic", ensuring we never brick
+user installs.
+
+For details about how, and what is done, read-on.
+
+## The problem with writing a file
+
+A write may very well be interrupted.
+
+While reading the resulting mangled file will typically break `json.Unmarshall` for example, and while we should still
+handle such cases gracefully and provide meaningful information to the user about which file is damaged (which could be due
+to the user manually modifying them), using "atomic" writes will (almost always (*)) prevent this from happening
+on our part.
+
+An "atomic" write is usually performed by first writing data to a temporary file, and then, only if the write operation
+succeeded, move that temporary file to its final destination.
+
+The `rename` syscall (see https://man7.org/linux/man-pages/man2/rename.2.html) is indeed "atomic"
+(eg: it fully succeeds, or fails), providing said guarantees that you end-up with a complete file that has the entirety
+of what was meant to be written.
+
+This is an "almost always", as an _operating system crash_ MAY break that promise (this is highly dependent on specifics
+that are out of scope here, and that nerdctl has no control over).
+Though, crashing operating systems is (hopefully) a sufficiently rare event that we can consider we "always" have atomic writes.
+
+There is one caveat with "rename-based atomic writes" though: if you mount the file itself inside a container,
+an atomic write will not work as you expect, as the inode will (obviously) change when you modify the file,
+and these changes will not be propagated inside the container.
+
+This caveat is the reason why `hostsstore` does NOT use an atomic write to update the `hosts` file, but a traditional write.
+
+## Concurrency between go routines
+
+This is a (simple) well-known problem. Just use a mutex to prevent concurrent modifications of the same object.
+
+Note that this is not much of a problem right now in nerdctl itself - but it might be in third-party applications using
+our codebase.
+
+This is just generally good hygiene when building concurrency-safe packages.
+
+## Concurrency between distinct binary invocations
+
+This is much more of a problem.
+
+There are many good reasons and real-life scenarios where concurrent binary execution may happen.
+A third-party deployment tool (similar to terraform for eg), that will batch a bunch of operations to be performed
+to achieve a desired infrastructure state, and call many `nerdctl` invocations in parallel to achieve that.
+This is also common-place in testing (subpackages).
+And of course, a third-party tool that would be long-running and allow parallel execution, leveraging nerdctl codebase
+as a library, may certainly produce these circumstances.
+
+The known answer to that problem is to use a filesystem lock (or flock).
+
+Concretely, the first process will "lock" a directory. All other processes trying to do the same will then be put
+in a queue and wait for the prior lock to be released before they can "lock" themselves, in turn.
+
+Filesystem locking comes with its own set of challenges:
+- implementation is somewhat low-level (the golang core library keeps their implementation internal, and you have to
+reimplement your own with platform-dependent APIs and syscalls)
+- it is tricky to get right - there are reasons why golang core does not make it public
+- locking "scope" should be done carefully: having ONE global lock for everything will definitely hurt performance badly,
+as you will basically make everything "sequential", effectively destroying some of the benefits of parallelizing code
+in the first place...
+
+## Lock design...
+
+While it is tempting to just provide self-locking, individual methods as an API (`Get`, `Set`), this is not the right
+answer.
+
+Imagine a context where consuming code would first like to check if something exists, then later on create it if it does not:
+```golang
+if !store.Exists("something") {
+ // do things
+ // ...
+ // Now, create
+ store.Set([]byte("data"), "something")
+}
+```
+
+You do have two methods (`Get` and `Set`) that _may individually_ guarantee they are the sole user of that resource,
+but a concurrent change _in between_ these two calls may very well (and _will_) happen and change the state of the world.
+
+Effectively, in that case, `Set` might overwrite changes made by another go routine or concurrent execution, possibly
+wrecking havoc in another process.
+
+_When_ to lock, and _for how long_, is a decision that only the embedding code can make.
+
+A good example is container creation.
+It may require the creation of several different volumes.
+In that case, you want to lock at the start of the container creation process, and only release the lock when you are fully
+done creating the container - not just when done creating a volume (nor even when done creating all volumes).
+
+## ... while safeguarding the developer
+
+nerdctl still provides some safeguards for the developer.
+
+Any store method that DOES require locking will fail loudly if it does not detect a lock.
+
+This is obviously not bullet-proof.
+For example, the lock may belong to another goroutine instead of the one we are in (and we cannot detect that).
+But this is still better than nothing, and will help developers making sure they **do** lock.
+
+## Using the `store` api to implement your own storage
+
+While - as mentioned above - the store does _not_ lock on its own, specific "stores implementations" may, and should,
+provide higher-level methods that best fit their data-model usage, and that **do** lock on their own.
+
+For example, the namestore (which is the simplest store), does provide three simple methods:
+- Acquire
+- Release
+- Rename
+
+Users of the `namestore` do not have to bother with locking. These methods are safe to use concurrently.
+
+This is a good example of how to leverage core store primitives to implement a developer friendly, safe storage for
+"something" (in that case "names").
+
+Finaly note an important point - mentioned above: locking should be done to the smallest possible "segment" of sub-directories.
+Specifically, any store should lock only - at most - resources under the _namespace_ being manipulated.
+
+For example, a container lifecycle storage should not lock out any other container, but only its own private directory.
+
+## Scope, ambitions and future
+
+`pkg/store` has no ambition whatsoever to be a generic solution, usable outside nerdctl.
+
+It is solely designed to fit nerdctl needs, and if it was to be made usable standalone, would probably have to be modified
+extensively, which is clearly out of scope here.
+
+Furthermore, there are already much more advanced generic solutions out there that you should use instead for outside-of-nerdctl projects.
+
+As for future, one nice thing we should consider is to implement read-only locks in addition to the exclusive, write-locks
+we currently use.
+The net benefit would be a performance boost in certain contexts (massively parallel, mostly read environments).
\ No newline at end of file
diff --git a/docs/dir.md b/docs/dir.md
index f0084f0b23d..4843eadb6bc 100644
--- a/docs/dir.md
+++ b/docs/dir.md
@@ -34,6 +34,7 @@ Files:
- `log-config.json`: used for storing the `--log-opts` map of `nerdctl run`
- `-json.log`: used by `nerdctl logs`
- `oci-hook.*.log`: logs of the OCI hook
+- `lifecycle.json`: used to store stateful information about the container that can only be retrieved through OCI hooks
### `//names/`
e.g. `/var/lib/nerdctl/1935db59/names/default`
@@ -64,5 +65,9 @@ Data volume
Can be overridden with `nerdctl --cni-netconfpath=` flag and environment variable `$NETCONFPATH`.
+At the top-level of , network (files) are shared accross all namespaces.
+Sub-folders inside are only available to the namespace bearing the same name,
+and its networks definitions are private.
+
Files:
- `nerdctl-.conflist`: CNI conf list created by nerdctl
diff --git a/docs/faq.md b/docs/faq.md
index 7280c503dba..c2313f9ce16 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -32,6 +32,7 @@
- [Containers do not automatically start after rebooting the host](#containers-do-not-automatically-start-after-rebooting-the-host)
- [Error `failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: unable to apply cgroup configuration: unable to start unit ... {Name:Slice Value:"user.slice"} {Name:Delegate Value:true} ... Permission denied: unknown`](#error-failed-to-create-shim-task-oci-runtime-create-failed-runc-create-failed-unable-to-start-container-process-unable-to-apply-cgroup-configuration-unable-to-start-unit--nameslice-valueuserslice-namedelegate-valuetrue--permission-denied-unknown)
- [How to uninstall ? / Can't remove `~/.local/share/containerd`](#how-to-uninstall---cant-remove-localsharecontainerd)
+ - [How to clean a dangling cache of buildkit?](#how-to-clean-a-dangling-cache-of-buildkit)
@@ -355,3 +356,15 @@ Run the following commands:
containerd-rootless-setuptool.sh uninstall
rootlesskit rm -rf ~/.local/share/containerd ~/.local/share/nerdctl ~/.config/containerd
```
+
+### How to clean a dangling cache of buildkit?
+
+`buildkit` cache directory is located at `$HOME/.local/share/buildkit/`
+in rootless mode, which has same folder structure `/var/lib/buildkit/` in
+root mode.
+
+You can clear the cache objects by running the following command:
+```
+nerdctl builder prune
+```
+The command produce a progress message of id and size of removed objects.
diff --git a/docs/freebsd.md b/docs/freebsd.md
index aa7b404da9e..4ad17aedba3 100644
--- a/docs/freebsd.md
+++ b/docs/freebsd.md
@@ -14,10 +14,16 @@ instructions in the respective repositories.
## Usage
-You can use the `knast/freebsd` image to run a standard FreeBSD 13 jail:
+You can use the `dougrabson/freebsd13.2-small` image to run a FreeBSD 13 jail:
```sh
-nerdctl run --net none -it knast/freebsd:13-STABLE
+nerdctl run --net none -it dougrabson/freebsd13.2-small
+```
+
+Alternatively use `--platform` parameter to run linux containers
+
+```sh
+nerdctl run --platform linux --net none -it amazonlinux:2
```
@@ -25,10 +31,3 @@ nerdctl run --net none -it knast/freebsd:13-STABLE
- :warning: CNI & CNI plugins are not yet ported to FreeBSD. The only supported
network type is `none`
-- :warning: buildkit is not yet ported to FreeBSD.
- - [ ] https://github.com/tonistiigi/fsutil/pull/109 - buildkit dependency
- - [ ] https://github.com/moby/moby/pull/42866 - buildkit dependency
-- :warning: Linuxulator containers support is
- WIP. https://github.com/containerd/nerdctl/issues/280 https://github.com/containerd/containerd/pull/5480
-
-- :bug: `nerdctl compose` commands currently don't work. https://github.com/containerd/containerd/pull/5991
diff --git a/docs/gpu.md b/docs/gpu.md
index cd200351a3c..009170c1a37 100644
--- a/docs/gpu.md
+++ b/docs/gpu.md
@@ -20,7 +20,7 @@ You can specify number of GPUs to use via `--gpus` option.
The following example exposes all available GPUs.
```
-nerdctl run -it --rm --gpus all nvidia/cuda:9.0-base nvidia-smi
+nerdctl run -it --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi
```
You can also pass detailed configuration to `--gpus` option as a list of key-value pairs. The following options are provided.
@@ -32,7 +32,7 @@ You can also pass detailed configuration to `--gpus` option as a list of key-val
The following example exposes a specific GPU to the container.
```
-nerdctl run -it --rm --gpus '"capabilities=utility,compute",device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' nvidia/cuda:9.0-base nvidia-smi
+nerdctl run -it --rm --gpus '"capabilities=utility,compute",device=GPU-3a23c669-1f69-c64e-cf85-44e9b07e7a2a' nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi
```
## Fields for `nerdctl compose`
@@ -53,7 +53,7 @@ The following exposes all available GPUs to the container.
version: "3.8"
services:
demo:
- image: nvidia/cuda:9.0-base
+ image: nvidia/cuda:12.3.1-base-ubuntu20.04
command: nvidia-smi
deploy:
resources:
@@ -62,3 +62,24 @@ services:
- capabilities: ["utility"]
count: all
```
+
+## Trouble Shooting
+
+### `nerdctl run --gpus` fails when using the Nvidia gpu-operator
+
+If the Nvidia driver is installed by the [gpu-operator](https://github.com/NVIDIA/gpu-operator).The `nerdctl run` will fail with the error message `(FATA[0000] exec: "nvidia-container-cli": executable file not found in $PATH)`.
+
+So, the `nvidia-container-cli` needs to be added to the PATH environment variable.
+
+You can do this by adding the following line to your $HOME/.profile or /etc/profile (for a system-wide installation):
+```
+export PATH=$PATH:/usr/local/nvidia/toolkit
+```
+
+The shared libraries also need to be added to the system.
+```
+echo "/run/nvidia/driver/usr/lib/x86_64-linux-gnu" > /etc/ld.so.conf.d/nvidia.conf
+ldconfig
+```
+
+And then, the `nerdctl run --gpus` can run successfully.
diff --git a/docs/images/nerdctl-white.svg b/docs/images/nerdctl-white.svg
new file mode 100644
index 00000000000..8edeab006a5
--- /dev/null
+++ b/docs/images/nerdctl-white.svg
@@ -0,0 +1,25 @@
+
+
diff --git a/docs/images/nerdctl.svg b/docs/images/nerdctl.svg
new file mode 100644
index 00000000000..e4d9789beed
--- /dev/null
+++ b/docs/images/nerdctl.svg
@@ -0,0 +1,25 @@
+
+
diff --git a/docs/images/rootlessKit-network-design.png b/docs/images/rootlessKit-network-design.png
new file mode 100644
index 00000000000..5a1696b7095
Binary files /dev/null and b/docs/images/rootlessKit-network-design.png differ
diff --git a/docs/ipfs.md b/docs/ipfs.md
index 29a98524a1b..488a1488f90 100644
--- a/docs/ipfs.md
+++ b/docs/ipfs.md
@@ -121,7 +121,7 @@ localhost:5050/ipfs/
Here, `CID` is the IPFS CID of the image.
-:information_source: In the futural version of nerdctl and BuildKit, `ipfs://` prefix should be supported in Dockerfile.
+:information_source: In the future version of nerdctl and BuildKit, `ipfs://` prefix should be supported in Dockerfile.
Using this image reference, you can build an image on IPFS.
@@ -179,7 +179,7 @@ By default, nerdctl exposes the registry at `localhost:5050` (configurable via f
Optionally you can create systemd unit file of `nerdctl ipfs registry serve`.
An example systemd unit file for `nerdctl ipfs registry serve` can be the following.
-`nerdctl ipfs registry serve` is aware of environemnt variables for configuring the behaviour (e.g. listening port) so you can use `EnvironmentFile` for configuring it.
+`nerdctl ipfs registry serve` is aware of environment variables for configuring the behaviour (e.g. listening port) so you can use `EnvironmentFile` for configuring it.
```
[Unit]
diff --git a/docs/multi-platform.md b/docs/multi-platform.md
index 22113eeccf7..e2213c36482 100644
--- a/docs/multi-platform.md
+++ b/docs/multi-platform.md
@@ -11,7 +11,7 @@ e.g., ARM on Intel, and vice versa.
```console
$ sudo systemctl start containerd
-$ sudo nerdctl run --privileged --rm tonistiigi/binfmt --install all
+$ sudo nerdctl run --privileged --rm tonistiigi/binfmt:master --install all
$ ls -1 /proc/sys/fs/binfmt_misc/qemu*
/proc/sys/fs/binfmt_misc/qemu-aarch64
diff --git a/docs/notation.md b/docs/notation.md
index 34cd7682365..8e155c84e93 100644
--- a/docs/notation.md
+++ b/docs/notation.md
@@ -10,7 +10,7 @@ under the hood with make use of flags `--sign` while pushing the container image
container image.
* Ensure notation executable in your `$PATH`.
-* You can install notation by following this page: https://notaryproject.dev/docs/installation/cli/
+* You can install notation by following this page: https://notaryproject.dev/docs/user-guides/installation/cli/
* Notation follows the RC of OCI spec v1.1.0. Follow the [instruction](https://notaryproject.dev/docs/quickstart/#create-an-oci-compatible-registry) to set up the local registry with the compliance for testing purpose.
Prepare your environment:
@@ -50,7 +50,7 @@ $ nerdctl push --sign=notation --notation-key-name test localhost:5000/my-test
Verify the container image while pulling:
-> REMINDER: Image won't be pulled if there are no matching signatures with the cert in the [trust policy](https://notaryproject.dev/docs/concepts/trust-store-trust-policy-specification/#trust-policy) in case you passed `--verify` flag.
+> REMINDER: Image won't be pulled if there are no matching signatures with the cert in the [trust policy](https://github.com/notaryproject/specifications/blob/main/specs/trust-store-trust-policy.md#trust-policy) in case you passed `--verify` flag.
```shell
# Create `trustpolicy.json` under $XDG_CONFIG_HOME/notation (XDG_CONFIG_HOME is ~/.config below)
diff --git a/docs/nydus.md b/docs/nydus.md
index 338f09f1639..1019827a548 100644
--- a/docs/nydus.md
+++ b/docs/nydus.md
@@ -30,7 +30,7 @@ For the list of pre-converted Nydus images, see https://github.com/orgs/dragonfl
Nerdctl supports to convert an OCI image or docker format v2 image to Nydus image by using the `nerdctl image convert` command.
-Before the conversion, you should have the `nydus-image` binary installed, which is contained in the ["nydus static package"](https://github.com/dragonflyoss/image-service/releases). You can run the command like `nerdctl image convert --nydus --oci --nydus-image ` to convert the `` to a Nydus image whose tag is ``.
+Before the conversion, you should have the `nydus-image` binary installed, which is contained in the ["nydus static package"](https://github.com/dragonflyoss/image-service/releases). You can run the command like `nerdctl image convert --nydus --oci --nydus-builder-path ` to convert the `` to a Nydus image whose tag is ``.
By now, the converted Nydus image cannot be run directly. It shoud be unpacked to nydus snapshotter before `nerdctl run`, which is a part of the processing flow of `nerdctl image pull`. So you need to push the converted image to a registry after the conversion and use `nerdctl --snapshotter nydus image pull` to unpack it to the nydus snapshotter before running the image.
diff --git a/docs/registry.md b/docs/registry.md
index 299809e26f2..b74e1ffa18b 100644
--- a/docs/registry.md
+++ b/docs/registry.md
@@ -49,6 +49,7 @@ See https://github.com/containerd/nerdctl/issues/86 for the discussion about wor
+
- [Amazon Elastic Container Registry (ECR)](#amazon-elastic-container-registry-ecr)
- [Logging in](#logging-in)
- [Creating a repo](#creating-a-repo)
@@ -74,7 +75,7 @@ See https://github.com/containerd/nerdctl/issues/86 for the discussion about wor
- [Logging in](#logging-in-5)
- [Creating a repo](#creating-a-repo-5)
- [Pushing an image](#pushing-an-image-5)
-- [Google Container Registry (GCR)](#google-container-registry-gcr)
+- [Google Container Registry (GCR) [DEPRECATED]](#google-container-registry-gcr-deprecated)
- [Logging in](#logging-in-6)
- [Creating a repo](#creating-a-repo-6)
- [Pushing an image](#pushing-an-image-6)
@@ -280,7 +281,7 @@ Create a [GCP Service Account](https://cloud.google.com/iam/docs/creating-managi
Then run the following command:
```console
-$ cat | docker login -u _json_key --password-stdin https://-docker.pkg.dev
+$ cat | nerdctl login -u _json_key --password-stdin https://-docker.pkg.dev
WARNING! Your password will be stored unencrypted in /home//.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
@@ -288,7 +289,7 @@ https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
```
-See also https://cloud.google.com/artifact-registry/docs/docker/authentication#json-key
+See also https://cloud.google.com/artifact-registry/docs/docker/authentication
@@ -336,7 +337,7 @@ $ nerdctl push -docker.pkg.dev///hello-world
The pushed image appears in the repository you manually created in the previous step.
-## Google Container Registry (GCR)
+## Google Container Registry (GCR) [DEPRECATED]
See also https://cloud.google.com/container-registry/docs/advanced-authentication
### Logging in
@@ -347,7 +348,7 @@ Create a [GCP Service Account](https://cloud.google.com/iam/docs/creating-managi
Then run the following command:
```console
-$ cat | docker login -u _json_key --password-stdin https://asia.gcr.io
+$ cat | nerdctl login -u _json_key --password-stdin https://asia.gcr.io
WARNING! Your password will be stored unencrypted in /home//.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
diff --git a/docs/rootless.md b/docs/rootless.md
index 4c9cfa5d4b1..1000bd50865 100644
--- a/docs/rootless.md
+++ b/docs/rootless.md
@@ -25,6 +25,10 @@ The usage of `containerd-rootless-setuptool.sh` is almost same as [`dockerd-root
Resource limitation flags such as `nerdctl run --memory` require systemd and cgroup v2: https://rootlesscontaine.rs/getting-started/common/cgroup2/
+#### AppArmor Profile for Ubuntu 24.04+
+
+Configuring AppArmor is needed only on Ubuntu 24.04+, with RootlessKit installed under a non-standard path: https://rootlesscontaine.rs/getting-started/common/apparmor/
+
## Client (nerdctl)
Just execute `nerdctl`. No need to specify the socket address manually.
@@ -109,7 +113,7 @@ See https://github.com/containerd/stargz-snapshotter/blob/main/docs/pre-converte
|-------------------|-----------------|
-[bypass4netns(https://github.com/rootless-containers/bypass4netns)](https://github.com/rootless-containers/bypass4netns) is an accelerator for rootless networking.
+[bypass4netns](https://github.com/rootless-containers/bypass4netns) is an accelerator for rootless networking.
This improves **outgoing or incoming (with --publish option) networking performance.**
@@ -121,16 +125,69 @@ The performance benchmark with iperf3 on Ubuntu 21.10 on Hyper-V VM is shown bel
This benchmark can be reproduced with [https://github.com/rootless-containers/bypass4netns/blob/f009d96139e9e38ce69a2ea8a9a746349bad273c/Vagrantfile](https://github.com/rootless-containers/bypass4netns/blob/f009d96139e9e38ce69a2ea8a9a746349bad273c/Vagrantfile)
-Acceleration with bypass4netns is available with `--label nerdctl/bypass4netns=true`. You also need to have `bypass4netnsd` (bypass4netns daemon) to be running.
+Acceleration with bypass4netns is available with:
+- `--annotation nerdctl/bypass4netns=true` (for nerdctl v2.0 and later)
+- `--label nerdctl/bypass4netns=true` (deprecated form, used in nerdctl prior to v2.0).
+
+You also need to have `bypass4netnsd` (bypass4netns daemon) to be running.
Example
```console
$ containerd-rootless-setuptool.sh install-bypass4netnsd
-$ nerdctl run -it --rm -p 8080:80 --label nerdctl/bypass4netns=true alpine
+$ nerdctl run -it --rm -p 8080:80 --annotation nerdctl/bypass4netns=true alpine
```
More detail is available at [https://github.com/rootless-containers/bypass4netns/blob/master/README.md](https://github.com/rootless-containers/bypass4netns/blob/master/README.md)
+## Configuring RootlessKit
+
+Rootless containerd recognizes the following environment variables to configure the behavior of [RootlessKit](https://github.com/rootless-containers/rootlesskit):
+
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR`: the rootlesskit state dir. Defaults to `$XDG_RUNTIME_DIR/containerd-rootless`.
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_NET=(slirp4netns|vpnkit|lxc-user-nic)`: the rootlesskit network driver. Defaults to "slirp4netns" if slirp4netns (>= v0.4.0) is installed. Otherwise defaults to "vpnkit".
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU=NUM`: the MTU value for the rootlesskit network driver. Defaults to 65520 for slirp4netns, 1500 for other drivers.
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns)`: the rootlesskit port driver. Defaults to "builtin" (this driver does not propagate the container's source IP address and always uses 127.0.0.1. Please check [Port Drivers](https://github.com/rootless-containers/rootlesskit/blob/master/docs/port.md#port-drivers) for more details).
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false)`: whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto".
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false)`: whether to protect slirp4netns with seccomp. Defaults to "auto".
+* `CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false)`: whether to launch rootlesskit with the "detach-netns" mode.
+ Defaults to "auto", which is resolved to "true" if RootlessKit >= 2.0 is installed.
+ The "detached-netns" mode accelerates `nerdctl (pull|push|build)` and enables `nerdctl run --net=host`,
+ however, there is a relatively minor drawback with BuildKit prior to v0.13:
+ the host loopback IP address (127.0.0.1) and abstract sockets are exposed to Dockerfile's "RUN" instructions during `nerdctl build` (not `nerdctl run`).
+ The drawback is fixed in BuildKit v0.13. Upgrading from a prior version of BuildKit needs removing the old systemd unit:
+ `containerd-rootless-setuptool.sh uninstall-buildkit && rm -f ~/.config/buildkit/buildkitd.toml`
+
+To set these variables, create `~/.config/systemd/user/containerd.service.d/override.conf` as follows:
+```ini
+[Service]
+Environment=CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS="false"
+```
+
+And then run the following commands:
+```bash
+systemctl --user daemon-reload
+systemctl --user restart containerd
+```
+
## Troubleshooting
### Hint to Fedora users
- If SELinux is enabled on your host and your kernel is older than 5.13, you need to use [`fuse-overlayfs` instead of `overlayfs`](#fuse-overlayfs).
+
+## Rootlesskit Network Design
+
+In `detach-netns` mode:
+
+- Network namespace is detached and stored in `$ROOTLESSKIT_STATE_DIR/netns`.
+- The child command executes within the host's network namespace, allowing actions like `pull` and `push` to happen in the host network namespace.
+- For creating and configuring the container's network namespace, the child command switches temporarily to the relevant namespace located in `$ROOTLESSKIT_STATE_DIR/netns`. This ensures necessary network setup while maintaining isolation in the host namespace.
+
+![rootlessKit-network-design.png](images/rootlessKit-network-design.png)
+
+- Rootlesskit Parent NetNS and Child NetNS are already configured by the startup script [containerd-rootless.sh](https://github.com/containerd/nerdctl/blob/main/extras/rootless/containerd-rootless.sh)
+- Rootlesskit Parent NetNS is the host network namespace
+- step1: `nerdctl` calls `containerd` in the host network namespace.
+- step2: `containerd` calls `runc` in the host network namespace.
+- step3: `runc` creates container with dedicated namespaces (e.g network ns) in the Parent netns.
+- step4: `runc` nsenter Rootlesskit Child NetNS before triggering nerdctl ocihook.
+- step5: `nerdctl` ocihook module leverages CNI.
+- step6: CNI configures container network namespace: create network interfaces `eth0` -> `veth0` -> `nerdctl0`.
diff --git a/docs/soci.md b/docs/soci.md
new file mode 100644
index 00000000000..67fbe92f584
--- /dev/null
+++ b/docs/soci.md
@@ -0,0 +1,47 @@
+# Lazy-pulling using SOCI Snapshotter
+
+SOCI Snapshotter is a containerd snapshotter plugin. It enables standard OCI images to be lazily loaded without requiring a build-time conversion step. "SOCI" is short for "Seekable OCI", and is pronounced "so-CHEE".
+
+See https://github.com/awslabs/soci-snapshotter to learn further information.
+
+## Prerequisites
+
+- Install containerd remote snapshotter plugin (`soci-snapshotter-grpc`) from https://github.com/awslabs/soci-snapshotter/blob/main/docs/getting-started.md
+
+- Add the following to `/etc/containerd/config.toml`:
+```toml
+[proxy_plugins]
+ [proxy_plugins.soci]
+ type = "snapshot"
+ address = "/run/soci-snapshotter-grpc/soci-snapshotter-grpc.sock"
+```
+
+- Launch `containerd` and `soci-snapshotter-grpc`
+
+## Enable SOCI for `nerdctl run` and `nerdctl pull`
+
+| :zap: Requirement | nerdctl >= 1.5.0 |
+| ----------------- | ---------------- |
+
+- Run `nerdctl` with `--snapshotter=soci`
+```console
+nerdctl run -it --rm --snapshotter=soci public.ecr.aws/soci-workshop-examples/ffmpeg:latest
+```
+
+- You can also only pull the image with SOCI without running the container.
+```console
+nerdctl pull --snapshotter=soci public.ecr.aws/soci-workshop-examples/ffmpeg:latest
+```
+
+For images that already have SOCI indices, see https://gallery.ecr.aws/soci-workshop-examples
+
+## Enable SOCI for `nerdctl push`
+
+| :zap: Requirement | nerdctl >= 1.6.0 |
+| ----------------- | ---------------- |
+
+- Push the image with SOCI index. Adding `--snapshotter=soci` arg to `nerdctl pull`, `nerdctl` will create the SOCI index and push the index to same destination as the image.
+```console
+nerdctl push --snapshotter=soci --soci-span-size=2097152 --soci-min-layer-size=20971520 public.ecr.aws/my-registry/my-repo:latest
+```
+--soci-span-size and --soci-min-layer-size are two properties to customize the SOCI index. See [Command Reference](https://github.com/containerd/nerdctl/blob/377b2077bb616194a8ef1e19ccde32aa1ffd6c84/docs/command-reference.md?plain=1#L773) for further details.
diff --git a/docs/stargz.md b/docs/stargz.md
index b54efb4bac4..5a54fe17906 100644
--- a/docs/stargz.md
+++ b/docs/stargz.md
@@ -175,9 +175,9 @@ You can use zstd compression with lazy pulling support (a.k.a zstd:chunked) inst
- [Faster](https://github.com/facebook/zstd/tree/v1.5.2#benchmarks) compression/decompression.
- Cons
- Old tools might not support. And unsupported by some tools yet.
- - zstd support by OCI Image Specification is still under rc (2022/11). will be added to [v1.1.0](https://github.com/opencontainers/image-spec/commit/1a29e8675a64a5cdd2d93b6fa879a82d9a4d926a).
- - zstd support unreleased [by Docker](https://github.com/moby/moby/pull/41759/commits/e187eb2bb5f0c3f899fe643e95d1af8c57e89a73) (will be added to v22.06).
- - [containerd >= v1.5](https://github.com/containerd/containerd/releases/tag/v1.5.0) supports zstd.
+ - zstd supported by OCI Image Specification is still under rc (2022/11). will be added to [v1.1.0](https://github.com/opencontainers/image-spec/commit/1a29e8675a64a5cdd2d93b6fa879a82d9a4d926a).
+ - zstd supported by [docker >=v23.0.0](https://github.com/moby/moby/releases/tag/v23.0.0).
+ - zstd supported by [containerd >= v1.5](https://github.com/containerd/containerd/releases/tag/v1.5.0).
- `min-chunk-size`, `external-toc` (described in Tips 1) are unsupported yet.
```console
diff --git a/docs/testing/README.md b/docs/testing/README.md
new file mode 100644
index 00000000000..c5bddc99285
--- /dev/null
+++ b/docs/testing/README.md
@@ -0,0 +1,118 @@
+# Testing nerdctl
+
+This document covers basic usage of nerdctl testing tasks, and generic recommendations
+and principles about writing tests.
+
+For more comprehensive information about nerdctl test tools, see [tools.md](tools.md).
+
+## Lint
+
+```
+go mod tidy
+golangci-lint run ./...
+```
+
+This works on macOS as well - just pass along `GOOS=linux`.
+
+## Unit testing
+
+Run `go test -v ./pkg/...`
+
+## Integration testing
+
+### TL;DR
+
+Be sure to first `make && sudo make install`
+
+```bash
+# Test all with nerdctl (rootless mode, if running go as a non-root user)
+go test -p 1 ./cmd/nerdctl/...
+
+# Test all with nerdctl rootful
+go test -p 1 -exec sudo ./cmd/nerdctl/...
+
+# Test all with docker
+go test -p 1 ./cmd/nerdctl/... -args -test.target=docker
+
+# Test just the tests(s) which names match TestVolume.*
+go test -p 1 ./cmd/nerdctl/... -run "TestVolume.*"
+# Or alternatively, just test the subpackage
+go test ./cmd/nerdctl/volume
+```
+
+### About parallelization
+
+By default, when `go test ./foo/...` finds subpackages, it does create _a separate test binary
+per sub-package_, and execute them _in parallel_.
+This effectively will make distinct tests in different subpackages to be executed in
+parallel, regardless of whether they called `t.Parallel` or not.
+
+The `-p 1` flag does inhibit this behavior, and forces go to run each sub-package
+sequentially.
+
+Note that this is different from the `--parallel` flag, which controls the amount of
+parallelization that a single go test binary will use when faced with tests that do
+explicitly allow it (with a call to `t.Parallel()`).
+
+### Or test in a container
+
+```bash
+docker build -t test-integration --target test-integration .
+docker run -t --rm --privileged test-integration
+```
+
+### Principles
+
+#### Tests should be parallelized (with best effort)
+
+##### General case
+
+It should be possible to parallelize all tests - as such, please make sure you:
+- name all resources your test is manipulating after the test identifier (`testutil.Identifier(t)`)
+to guarantee your test will not interact with other tests
+- do NOT use `os.Setenv` - instead, add into `base.Env`
+- use `t.Parallel()` at the beginning of your test (and subtests as well of course)
+- in the very exceptional case where your test for some reason can NOT be parallelized, be sure to mark it explicitly as such
+with a comment explaining why
+
+##### For "blanket" destructive operations
+
+If you are going to use blanket destructive operations (like `prune`), please:
+- use a dedicated namespace: instead of calling `testutil.Base`, call `testutil.BaseWithNamespace`
+and be sure that your namespace is named after the test id
+- remove the namespace in your test `Cleanup`
+- since docker does not support namespaces, be sure to:
+ - only enable `Parallel` if the target is NOT docker: ` if testutil.GetTarget() != testutil.Docker { t.Parallel() }`
+ - double check that what you do in the default namespace is safe
+
+#### Clean-up after (and before) yourself
+
+- do NOT use `defer`, use `t.Cleanup`
+- do NOT test the result of commands doing the cleanup - it is fine if they fail,
+and they are not test failure per-se - they are here to garbage collect
+- you should call your cleanup routine BEFORE doing anything, in case there is any
+leftovers from previous runs, typically:
+```
+tearDown := func(){
+ // Do some cleanup
+}
+
+tearDown()
+t.Cleanup(tearDown)
+```
+
+#### Test what you are testing, and not something else
+
+You should only test atomically.
+
+If you are testing `nerdctl volume create`, make sure that your test will not fail
+because of changes in `nerdctl volume inspect`.
+
+That obviously means there are certain things you cannot test "yet".
+Just put the right test in the right place with this simple rule of thumb:
+if your test requires another nerdctl command to validate the result, then it does
+not belong here. Instead, it should be a test for that other command.
+
+Of course, this is not perfect, and changes in `create` may now fail in `inspect` tests
+while `create` could be faulty, but it does beat the alternative, because of this principle:
+it is easier to walk *backwards* from a failure.
\ No newline at end of file
diff --git a/docs/testing/tools.md b/docs/testing/tools.md
new file mode 100644
index 00000000000..2f53441b30c
--- /dev/null
+++ b/docs/testing/tools.md
@@ -0,0 +1,437 @@
+# Nerdctl testing tools
+
+## Preamble
+
+The integration test suite in nerdctl is meant to apply to both nerdctl and docker,
+and further support additional test properties to target specific contexts (ipv6, kube).
+
+Basic _usage_ is covered in the [testing docs](README.md).
+
+This here covers how to write tests, leveraging nerdctl `pkg/testutil/test`
+which has been specifically developed to take care of repetitive tasks,
+protect the developer against unintended side effects across tests, and generally
+encourage clear testing structure with good debug-ability and a relatively simple API for
+most cases.
+
+## Using `test.Case`
+
+Starting from scratch, the simplest, basic structure of a new test is:
+
+```go
+package main
+
+import (
+ "testing"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMyThing(t *testing.T) {
+ // Declare your test
+ myTest := nerdtest.Setup()
+ // This is going to run `nerdctl info` (or `docker info`)
+ mytest.Command = test.Command("info")
+ // Verify the command exits with 0, and stdout contains the word `Kernel`
+ myTest.Expected = test.Expects(0, nil, test.Contains("Kernel"))
+ // Run it
+ myTest.Run(t)
+}
+```
+
+## Expectations
+
+There are a handful of helpers for "expectations".
+
+You already saw two (`test.Expects` and `test.Contains`):
+
+First, `test.Expects(exitCode int, errors []error, outputCompare Comparator)`, which is
+convenient to quickly describe what you expect overall.
+
+`exitCode` is obvious (note that passing -1 as an exit code will just
+verify the commands does fail without comparing the code, and -2 will not verify the exit
+code at all).
+
+`errors` is a slice of go `error`, that allows you to compare what is seen on stderr
+with existing errors (for example: `errdefs.ErrNotFound`), or more generally
+any string you want to match.
+
+`outputCompare` can be either your own comparison function, or
+one of the comparison helper.
+
+Secondly, `test.Contains` - which is a `Comparator`.
+
+### Comparators
+
+Besides `test.Contains(string)`, there are a few more:
+- `test.DoesNotContain(string)`
+- `test.Equals(string)`
+- `test.Match(*regexp.Regexp)`
+- `test.All(comparators ...Comparator)`, which allows you to bundle together a bunch of other comparators
+
+The following example shows how to implement your own custom `Comparator`
+(this is actually the `Equals` comparator).
+
+```go
+package whatever
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func MyComparator(compare string) test.Comparator {
+ return func(stdout string, info string, t *testing.T) {
+ t.Helper()
+ assert.Assert(t, stdout == compare, info)
+ }
+}
+```
+
+Note that you have access to an opaque `info` string.
+It contains relevant debugging information in case your comparator is going to fail,
+and you should make sure it is displayed.
+
+### Advanced expectations
+
+You may want to have expectations that contain a certain piece of data
+that is being used in the command or at other stages of your test (Setup).
+
+For example, creating a container with a certain name, you might want to verify
+that this name is then visible in the list of containers.
+
+To achieve that, you should write your own `Manager`, leveraging test `Data`.
+
+Here is an example, where we are using `data.Get("sometestdata")`.
+
+```go
+package main
+
+import (
+ "errors"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMyThing(t *testing.T) {
+ nerdtest.Setup()
+
+ // Declare your test
+ myTest := &test.Case{
+ Data: test.WithData("sometestdata", "blah"),
+ Command: test.Command("info"),
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{
+ errors.New("foobla"),
+ errdefs.ErrNotFound,
+ },
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, stdout == data.Get("sometestdata"), info)
+ },
+ }
+ },
+ }
+
+ myTest.Run(t)
+}
+```
+
+## On `Data`
+
+`Data` is provided to allow storing mutable key-value information that pertain to the test.
+
+While it can be provided through `test.WithData(key string, value string)`,
+inside the testcase definition, it can also be dynamically manipulated inside `Setup`, or `Command`.
+
+Note that `Data` additionally exposes the following functions:
+- `Identifier(words ...string)` which returns a unique identifier associated with the _current_ test (or subtest)
+- `TempDir()` which returns the private, temporary directory associated with the test
+
+... along with the `Get(key)` and `Set(key, value)` methods.
+
+Note that Data is copied down to subtests, which is convenient to pass "down"
+information relevant to a bunch of subtests (eg: like a registry IP).
+
+## On Config
+
+`Config` is similar to `Data`, although it is meant specifically for predefined
+keys that impact the base behavior of the binary you are testing.
+
+You can initiate your config using `test.WithConfig(key, value)`, and you can
+manipulate it further using `helpers.Read` and`helpers.Write`.
+
+Currently, the following keys are defined:
+- `DockerConfig` allowing to set custom content for the `$DOCKER_CONFIG/config.json` file
+- `Namespace` (default to `nerdctl-test` if unspecified, but see "mode private")
+- `NerdctlToml` to set custom content for the `$NERDCTL_TOML` file
+- `HostsDir` to specify the value of the arg `--hosts-dir`
+- `DataRoot` to specify the value of the arg `--data-root`
+- `Debug` to enable debug (works for both nerdctl and docker)
+
+Note that config defined on the test case is copied over for subtests.
+
+## Commands
+
+For simple cases, `test.Command(args ...string)` is the way to go.
+
+It will execute the binary to test (nerdctl or docker), with the provided arguments,
+and will by default get cwd inside the temporary directory associated with the test.
+
+### Environment
+
+You can attach custom environment variables for your test in the `Env` property of your
+test.
+
+These will be automatically added to the environment for your command, and also
+your setup and cleanup routines (see below).
+
+If you would like to override the environment specifically for a command, but not for
+others (eg: in `Setup` or `Cleanup`), you can do so with custom commands (see below).
+
+Note that environment as defined statically in the test will be copied over for subtests.
+
+### Working directory
+
+By default, the working directory of the command will be set to the temporary directory
+of the test.
+
+This behavior can be overridden using custom commands.
+
+### Custom Executor
+
+Custom `Executor`s allow you to manipulate test `Data`, override important aspects
+of the command to execute (`Env`, `WorkingDir`), or otherwise give you full control
+on what the command does.
+
+You just need to implement an `Executor`:
+
+```go
+package main
+
+import (
+ "errors"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMyThing(t *testing.T) {
+ nerdtest.Setup()
+
+ // Declare your test
+ myTest := &test.Case{
+ Data: test.WithData("sometestdata", "blah"),
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--name", data.Get("sometestdata"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{
+ errors.New("foobla"),
+ errdefs.ErrNotFound,
+ },
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, stdout == data.Get("sometestdata"), info)
+ },
+ }
+ },
+ }
+
+ myTest.Run(t)
+}
+```
+
+Note that inside your `Executor` you do have access to the full palette of command options,
+including:
+- `Background(timeout time.Duration)` which allows you to background a command execution
+- `WithWrapper(binary string, args ...string)` which allows you to "wrap" your command with another binary
+- `WithStdin(io.Reader)` which allows you to pass a reader to the command stdin
+- `WithCwd(string)` which allows you to specify the working directory (default to the test temp directory)
+- `Clone()` which returns a copy of the command, with env, cwd, etc
+
+and also `WithBinary` and `WithArgs`.
+
+### On `helpers`
+
+Inside a custom `Executor`, `Manager`, or `Butler`, you have access to a collection of
+`helpers` to simplify command execution:
+
+- `helpers.Ensure(args ...string)` will run a command and ensure it exits successfully
+- `helpers.Fail(args ...string)` will run a command and ensure it fails
+- `helpers.Anyhow(args ...string)` will run a command but does not care if it succeeds or fails
+- `helpers.Capture(args ...string)` will run a command, ensure it is successful, and return the output
+- `helpers.Command(args ...string)` will return a command that can then be tested against expectations
+- `helpers.Custom(binary string, args ...string)` will do the same for any arbitrary command (not limited to nerdctl)
+- `helpers.T()` which returns the appropriate `*testing.T` for your context
+
+## Setup and Cleanup
+
+Tests routinely require a set of actions to be performed _before_ you can run the
+command you want to test.
+A setup routine will get executed before your `Command`, and have access to and can
+manipulate your test `Data` and `Config`.
+
+Conversely, you very likely want to clean things up once your test is done.
+While temporary directories are cleaned for you with no action needed on your part,
+the app you are testing might have stateful data you may want to remove.
+Note that a `Cleanup` routine will get executed twice - after your `Command` has run
+its course evidently - but also, pre-emptively, before your `Setup`, so that possible leftovers from
+previous runs are taken care of.
+
+```go
+package main
+
+import (
+ "errors"
+ "testing"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
+ "github.com/containerd/nerdctl/v2/pkg/testutil/test"
+)
+
+func TestMyThing(t *testing.T) {
+ nerdtest.Setup()
+
+ // Declare your test
+ myTest := &test.Case{
+ Data: test.WithData("sometestdata", "blah"),
+ Setup: func(data *test.Data, helpers test.Helpers){
+ helpers.Ensure("volume", "create", "foo")
+ helpers.Ensure("volume", "create", "bar")
+ },
+ Cleanup: func(data *test.Data, helpers test.Helpers){
+ helpers.Anyhow("volume", "rm", "foo")
+ helpers.Anyhow("volume", "rm", "bar")
+ },
+ Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
+ return helpers.Command("run", "--name", data.Identifier()+data.Get("sometestdata"))
+ },
+ Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
+ return &test.Expected{
+ ExitCode: 1,
+ Errors: []error{
+ errors.New("foobla"),
+ errdefs.ErrNotFound,
+ },
+ Output: func(stdout string, info string, t *testing.T) {
+ assert.Assert(t, stdout == data.Get("sometestdata"), info)
+ },
+ }
+ },
+ }
+
+ myTest.Run(t)
+}
+```
+
+## Subtests
+
+Subtests are just regular tests, attached to the `SubTests` slice of a test.
+
+Note that a subtest will inherit its parent `Data`, `Config` and `Env`, in the state they are at
+after the parent test has run its `Setup` and `Command` routines (but before `Cleanup`).
+This does _not_ apply to `Identifier()` and `TempDir()`, which are unique to the subtest.
+
+Also note that a test does not have to have a `Command`.
+This is a convenient pattern if you just need a common `Setup` for a bunch of subtests.
+
+## Parallelism
+
+All tests (and subtests) are assumed to be parallelizable.
+
+You can force a specific `test.Case` to not be run in parallel though,
+by setting its `NoParallel` property to `true`.
+
+Note that if you want better isolation, it is usually better to use the requirement
+`nerdtest.Private` instead of `NoParallel` (see below).
+
+## Requirements
+
+`test.Case` has a `Require` property that allow enforcing specific, per-test requirements.
+
+A `Requirement` is expected to make you `Skip` tests when the environment does not match
+expectations.
+
+Here are a few:
+```go
+test.Windows // a test runs only on Windows (or Not(Windows))
+test.Linux // a test runs only on Linux
+test.Darwin // a test runs only on Darwin
+test.OS(name string) // a test runs only on the OS `name`
+test.Binary(name string) // a test requires the bin `name` to be in the PATH
+test.Not(req Requirement) // a test runs only if the opposite of the requirement `req` is fulfilled
+test.Require(req ...Requirement) // a test runs only if all requirements are fulfilled
+
+nerdtest.Docker // a test only run on Docker - normally used with test.Not(nerdtest.Docker)
+nerdtest.Soci // a test requires the soci snapshotter
+nerdtest.Stargz // a test requires the stargz snapshotter
+nerdtest.Rootless // a test requires Rootless
+nerdtest.Rootful // a test requires Rootful
+nerdtest.Build // a test requires buildkit
+nerdtest.CGroup // a test requires cgroup
+nerdtest.NerdctlNeedsFixing // indicates that a test cannot be run on nerdctl yet as a fix is required
+nerdtest.BrokenTest // indicates that a test needs to be fixed and has been restricted to run only in certain cases
+nerdtest.OnlyIPv6 // a test is meant to run solely in the ipv6 environment
+nerdtest.OnlyKubernetes // a test is meant to run solely in the Kubernetes environment
+nerdtest.IsFlaky // indicates that a test will fail in a flaky way - this may be the test fault, or more likely something racy in nerdctl
+nerdtest.Private // see below
+```
+
+### About `nerdtest.Private`
+
+While all requirements above are self-descriptive or obvious, `nerdtest.Private` is a
+special case.
+
+If set, it will run tests inside a dedicated namespace that is private to the test.
+Note that subtests by default are going to be set in that same namespace, unless they
+ask for private as well, or they reset the `Namespace` config key.
+
+If the target is Docker - which does not support namespaces - asking for `Private`
+will disable parallelization.
+
+The purpose of private is to provide a truly clean-room environment for tests
+that are going to have side effects on others, or that do require an exclusive, pristine
+environment.
+
+Using private is generally preferable to disabling parallelization, as doing the latter
+would slow down the run and won't have the same isolation guarantees about the environment.
+
+## Advanced command customization
+
+Testing any non-trivial binary likely assume a good amount of custom code
+to set up the right default behavior wrt environment, flags, etc.
+
+To do that, you can pass a `test.Testable` implementation to the `test.Customize` method.
+
+It basically lets you define your own `CustomizableCommand`, along with a hook to deal with
+ambient requirements that is run after `test.Require` and before `test.Setup`.
+
+`CustomizableCommand` are typically embedding a `test.GenericCommand` and overriding both the
+`Run` and `Clone` methods.
+
+Check the `nerdtest` implementation for details.
+
+## Utilities
+
+TBD
\ No newline at end of file
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/README.md b/examples/nerdctl-ipfs-registry-kubernetes/README.md
index 4ff6c1d341d..f5061252b3b 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/README.md
+++ b/examples/nerdctl-ipfs-registry-kubernetes/README.md
@@ -6,9 +6,9 @@ This directory contains examples of node-to-node image sharing on Kubernetes wit
- [`./ipfs-cluster`](./ipfs-cluster): node-to-node image sharing with content replication using ipfs-cluster
- [`./ipfs-stargz-snapshotter`](./ipfs-stargz-snapshotter): node-to-node image sharing with lazy pulling using eStargz and Stargz Snapshotter
-## Example Dockerfile of `nerdctl ipfs regisry`
+## Example Dockerfile of `nerdctl ipfs registry`
-The above examples use `nerdctl ipfs regisry` running in a Pod.
+The above examples use `nerdctl ipfs registry` running in a Pod.
The image is available at [`ghcr.io/stargz-containers/nerdctl-ipfs-registry`](https://github.com/orgs/stargz-containers/packages/container/package/nerdctl-ipfs-registry).
The following Dockerfile can be used to build it by yourself.
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md
index c56f74ae7b7..05bd41fdc67 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/README.md
@@ -36,7 +36,7 @@ Prepare `kind-worker` (1st node) for importing an image to IPFS
```console
$ docker exec -it kind-worker /bin/bash
(kind-worker)# NERDCTL_VERSION=0.23.0
-(kind-worker)# curl -sSL --output /tmp/nerdctl.tgz https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
+(kind-worker)# curl -o /tmp/nerdctl.tgz -fsSL --proto '=https' --tlsv1.2 https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
(kind-worker)# tar zxvf /tmp/nerdctl.tgz -C /usr/local/bin/
```
@@ -55,7 +55,7 @@ $ docker exec -it kind-worker /bin/bash
The image added to `kind-worker` is shared to other nodes via IPFS.
You can run this image on the nodes using the following manifest.
-CID of the pushed image is printed when `nerdctl push` is succeded (we assume that the image is added to IPFS as CID `bafkreictyyoysj56v772xbfhyfrcvmgmfpa4vodmqaroz53ytvai7nof6u`).
+CID of the pushed image is printed when `nerdctl push` is succeeded (we assume that the image is added to IPFS as CID `bafkreictyyoysj56v772xbfhyfrcvmgmfpa4vodmqaroz53ytvai7nof6u`).
```console
$ cat <"${TMPIDFILE}" | base64 -w 0)
-ID=$(cat "${TMPIDFILE}" | grep "ID " | sed -E 's/[^:]*: (.*)/\1/')
+ID=$(grep "ID " "${TMPIDFILE}" | sed -E 's/[^:]*: (.*)/\1/')
rm "${TMPIDFILE}"
BOOTSTRAP_PEER_PRIV_KEY=$(echo "${BOOTSTRAP_KEY}" | base64 -w 0)
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/nerdctl-ipfs-registry.yaml b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/nerdctl-ipfs-registry.yaml
index 8a7d350f780..3e9a9743f09 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/nerdctl-ipfs-registry.yaml
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-cluster/nerdctl-ipfs-registry.yaml
@@ -310,7 +310,7 @@ data:
# wait for ipfs daemon
ok=false
for i in $(seq 100) ; do
- if curl localhost:9095/api/v0/id >/dev/null 2>&1 ; then
+ if curl -fsSL localhost:9095/api/v0/id >/dev/null 2>&1 ; then
ok=true
break
fi
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md
index 74648395b08..80db2fcd4f5 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/README.md
@@ -49,7 +49,7 @@ Prepare `kind-worker` (1st node) for importing an image to IPFS
```console
$ docker exec -it kind-worker /bin/bash
(kind-worker)# NERDCTL_VERSION=0.23.0
-(kind-worker)# curl -sSL --output /tmp/nerdctl.tgz https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
+(kind-worker)# curl -o /tmp/nerdctl.tgz -fsSL --proto '=https' --tlsv1.2 https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
(kind-worker)# tar zxvf /tmp/nerdctl.tgz -C /usr/local/bin/
```
@@ -68,7 +68,7 @@ $ docker exec -it kind-worker /bin/bash
The eStargz image added to `kind-worker` is shared to `kind-worker2` via IPFS.
You can perform lazy pulling of this eStargz image among nodes using the following manifest.
-CID of the pushed image is printed when `nerdctl push` is succeded (we assume that the image is added to IPFS as CID `bafkreidqrxutnnuc3oilje27px5o3gggzrfyomumrprcavr7nquoy3cdje`).
+CID of the pushed image is printed when `nerdctl push` is succeeded (we assume that the image is added to IPFS as CID `bafkreidqrxutnnuc3oilje27px5o3gggzrfyomumrprcavr7nquoy3cdje`).
```console
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/bootstrap.yaml.sh b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/bootstrap.yaml.sh
index 8905f617171..fc06a298e26 100755
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/bootstrap.yaml.sh
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/bootstrap.yaml.sh
@@ -19,12 +19,10 @@
set -eu -o pipefail
-for d in ipfs-swarm-key-gen ; do
- if ! command -v $d >/dev/null 2>&1 ; then
- echo "$d not found"
- exit 1
- fi
-done
+if ! command -v ipfs-swarm-key-gen >/dev/null 2>&1 ; then
+ echo "ipfs-swarm-key-gen not found"
+ exit 1
+fi
SWARM_KEY=$(ipfs-swarm-key-gen | base64 | tr -d '\n')
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/nerdctl-ipfs-registry.yaml b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/nerdctl-ipfs-registry.yaml
index f2f8551104a..6da5096546f 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/nerdctl-ipfs-registry.yaml
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs-stargz-snapshotter/nerdctl-ipfs-registry.yaml
@@ -193,7 +193,7 @@ data:
# wait for ipfs daemon
ok=false
for i in $(seq 100) ; do
- if curl localhost:5001/api/v0/id >/dev/null 2>&1 ; then
+ if curl -fsSL localhost:5001/api/v0/id >/dev/null 2>&1 ; then
ok=true
break
fi
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md
index 67907c5783c..53ef383802f 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/README.md
@@ -34,7 +34,7 @@ Prepare `kind-worker` (1st node) for importing an image to IPFS
```console
$ docker exec -it kind-worker /bin/bash
(kind-worker)# NERDCTL_VERSION=0.23.0
-(kind-worker)# curl -sSL --output /tmp/nerdctl.tgz https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
+(kind-worker)# curl -fsSL --proto '=https' --tlsv1.2 --output /tmp/nerdctl.tgz https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz
(kind-worker)# tar zxvf /tmp/nerdctl.tgz -C /usr/local/bin/
```
@@ -51,7 +51,7 @@ $ docker exec -it kind-worker /bin/bash
The image added to `kind-worker` is shared to `kind-worker2` via IPFS.
You can run this image on all worker nodes using the following manifest.
-CID of the pushed image is printed when `nerdctl push` is succeded (we assume that the image is added to IPFS as CID `bafkreictyyoysj56v772xbfhyfrcvmgmfpa4vodmqaroz53ytvai7nof6u`).
+CID of the pushed image is printed when `nerdctl push` succeeded (we assume that the image is added to IPFS as CID `bafkreictyyoysj56v772xbfhyfrcvmgmfpa4vodmqaroz53ytvai7nof6u`).
```console
$ cat </dev/null 2>&1 ; then
- echo "$d not found"
- exit 1
- fi
-done
+if ! command -v ipfs-swarm-key-gen >/dev/null 2>&1 ; then
+ echo "ipfs-swarm-key-gen not found"
+ exit 1
+fi
SWARM_KEY=$(ipfs-swarm-key-gen | base64 | tr -d '\n')
diff --git a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/nerdctl-ipfs-registry.yaml b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/nerdctl-ipfs-registry.yaml
index f2f8551104a..6da5096546f 100644
--- a/examples/nerdctl-ipfs-registry-kubernetes/ipfs/nerdctl-ipfs-registry.yaml
+++ b/examples/nerdctl-ipfs-registry-kubernetes/ipfs/nerdctl-ipfs-registry.yaml
@@ -193,7 +193,7 @@ data:
# wait for ipfs daemon
ok=false
for i in $(seq 100) ; do
- if curl localhost:5001/api/v0/id >/dev/null 2>&1 ; then
+ if curl -fsSL localhost:5001/api/v0/id >/dev/null 2>&1 ; then
ok=true
break
fi
diff --git a/extras/rootless/containerd-rootless-setuptool.sh b/extras/rootless/containerd-rootless-setuptool.sh
index 6a26d07f639..27627640d51 100755
--- a/extras/rootless/containerd-rootless-setuptool.sh
+++ b/extras/rootless/containerd-rootless-setuptool.sh
@@ -29,19 +29,15 @@ set -eu
# utility functions
INFO() {
- # https://github.com/koalaman/shellcheck/issues/1593
- # shellcheck disable=SC2039
- /bin/echo -e "\e[104m\e[97m[INFO]\e[49m\e[39m ${*}"
+ printf "\e[104m\e[97m[INFO]\e[49m\e[39m %s\n" "$*"
}
WARNING() {
- # shellcheck disable=SC2039
- /bin/echo >&2 -e "\e[101m\e[97m[WARNING]\e[49m\e[39m ${*}"
+ >&2 printf "\e[101m\e[97m[WARNING]\e[49m\e[39m %s\n" "$*"
}
ERROR() {
- # shellcheck disable=SC2039
- /bin/echo >&2 -e "\e[101m\e[97m[ERROR]\e[49m\e[39m ${*}"
+ >&2 printf "\e[101m\e[97m[ERROR]\e[49m\e[39m %s\n" "$*"
}
# constants
@@ -139,18 +135,33 @@ cmd_entrypoint_check() {
INFO "Requirements are satisfied"
}
+propagate_env_from() {
+ pid="$1"
+ env="$(sed -e "s/\x0/'\n/g" <"/proc/${pid}/environ" | sed -Ee "s/^[^=]*=/export \0'/g")"
+ shift
+ for key in "$@"; do
+ eval "$(echo "$env" | grep "^export ${key=}")"
+ done
+}
+
# CLI subcommand: "nsenter"
cmd_entrypoint_nsenter() {
# No need to call init()
pid=$(cat "$XDG_RUNTIME_DIR/containerd-rootless/child_pid")
- exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m -n -U -t "$pid" -- "$@"
+ n=""
+ # If RootlessKit is running with `--detach-netns` mode, we do NOT enter the detached netns here
+ if [ ! -e "$XDG_RUNTIME_DIR/containerd-rootless/netns" ]; then
+ n="-n"
+ fi
+ propagate_env_from "$pid" ROOTLESSKIT_STATE_DIR ROOTLESSKIT_PARENT_EUID ROOTLESSKIT_PARENT_EGID
+ exec nsenter --no-fork --wd="$(pwd)" --preserve-credentials -m $n -U -t "$pid" -- "$@"
}
show_systemd_error() {
unit="$1"
n="20"
ERROR "Failed to start ${unit}. Run \`journalctl -n ${n} --no-pager --user --unit ${unit}\` to show the error log."
- ERROR "Before retrying installation, you might need to uninstall the current setup: \`$0 uninstall -f ; ${BIN}/rootlesskit rm -rf ${HOME}/.local/share/containerd\`"
+ ERROR "Before retrying installation, you might need to uninstall the current setup: \`$0 uninstall; ${BIN}/rootlesskit rm -rf ${HOME}/.local/share/containerd\`"
}
install_systemd_unit() {
@@ -215,6 +226,7 @@ cmd_entrypoint_install() {
cat <<-EOT | install_systemd_unit "${SYSTEMD_CONTAINERD_UNIT}"
[Unit]
Description=containerd (Rootless)
+ Requires=dbus.socket
[Service]
Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
@@ -255,6 +267,11 @@ cmd_entrypoint_install_buildkit() {
ERROR "Install containerd first (\`$ARG0 install\`)"
exit 1
fi
+ BUILDKITD_FLAG="--oci-worker=true --oci-worker-rootless=true --containerd-worker=false"
+ if buildkitd --help | grep -q bridge; then
+ # Available since BuildKit v0.13
+ BUILDKITD_FLAG="${BUILDKITD_FLAG} --oci-worker-net=bridge"
+ fi
cat <<-EOT | install_systemd_unit "${SYSTEMD_BUILDKIT_UNIT}"
[Unit]
Description=BuildKit (Rootless)
@@ -262,7 +279,7 @@ cmd_entrypoint_install_buildkit() {
[Service]
Environment=PATH=$BIN:/sbin:/usr/sbin:$PATH
- ExecStart="$REALPATH0" nsenter buildkitd
+ ExecStart="$REALPATH0" nsenter -- buildkitd ${BUILDKITD_FLAG}
ExecReload=/bin/kill -s HUP \$MAINPID
RestartSec=2
Restart=always
@@ -281,32 +298,25 @@ cmd_entrypoint_install_buildkit_containerd() {
ERROR "buildkitd (https://github.com/moby/buildkit) needs to be present under \$PATH"
exit 1
fi
- if [ ! -f "${XDG_CONFIG_HOME}/buildkit/buildkitd.toml" ]; then
- mkdir -p "${XDG_CONFIG_HOME}/buildkit"
- cat <<-EOF > "${XDG_CONFIG_HOME}/buildkit/buildkitd.toml"
- [worker.oci]
- enabled = false
-
- [worker.containerd]
- enabled = true
- rootless = true
- EOF
- fi
if ! systemctl --user --no-pager status "${SYSTEMD_CONTAINERD_UNIT}" >/dev/null 2>&1; then
ERROR "Install containerd first (\`$ARG0 install\`)"
exit 1
fi
UNIT_NAME=${SYSTEMD_BUILDKIT_UNIT}
- BUILDKITD_FLAG=
- if [ -n "${CONTAINERD_NAMESPACE:-}" ] ; then
+ BUILDKITD_FLAG="--oci-worker=false --containerd-worker=true --containerd-worker-rootless=true"
+ if [ -n "${CONTAINERD_NAMESPACE:-}" ]; then
UNIT_NAME="${CONTAINERD_NAMESPACE}-${SYSTEMD_BUILDKIT_UNIT}"
BUILDKITD_FLAG="${BUILDKITD_FLAG} --addr=unix://${XDG_RUNTIME_DIR}/buildkit-${CONTAINERD_NAMESPACE}/buildkitd.sock --root=${XDG_DATA_HOME}/buildkit-${CONTAINERD_NAMESPACE} --containerd-worker-namespace=${CONTAINERD_NAMESPACE}"
else
WARNING "buildkitd has access to images in \"buildkit\" namespace by default. If you want to give buildkitd access to the images in \"default\" namespace, run this command with CONTAINERD_NAMESPACE=default"
fi
- if [ -n "${CONTAINERD_SNAPSHOTTER:-}" ] ; then
+ if [ -n "${CONTAINERD_SNAPSHOTTER:-}" ]; then
BUILDKITD_FLAG="${BUILDKITD_FLAG} --containerd-worker-snapshotter=${CONTAINERD_SNAPSHOTTER}"
fi
+ if buildkitd --help | grep -q bridge; then
+ # Available since BuildKit v0.13
+ BUILDKITD_FLAG="${BUILDKITD_FLAG} --containerd-worker-net=bridge"
+ fi
cat <<-EOT | install_systemd_unit "${UNIT_NAME}"
[Unit]
Description=BuildKit (Rootless)
@@ -352,7 +362,7 @@ cmd_entrypoint_install_bypass4netnsd() {
[Install]
WantedBy=default.target
EOT
- INFO "To use bypass4netnsd, set the \"nerdctl/bypass4netns=true\" label on containers, e.g., \`nerdctl run --label nerdctl/bypass4netns=true\`"
+ INFO "To use bypass4netnsd, set the \"nerdctl/bypass4netns=true\" annotation on containers, e.g., \`nerdctl run --annotation nerdctl/bypass4netns=true\`"
}
# CLI subcommand: "install-fuse-overlayfs"
@@ -499,8 +509,14 @@ cmd_entrypoint_install_ipfs() {
cmd_entrypoint_uninstall() {
init
uninstall_systemd_unit "${SYSTEMD_BUILDKIT_UNIT}"
+ if [ -n "${CONTAINERD_NAMESPACE:-}" ]; then
+ uninstall_systemd_unit "${CONTAINERD_NAMESPACE}-${SYSTEMD_BUILDKIT_UNIT}"
+ fi
uninstall_systemd_unit "${SYSTEMD_FUSE_OVERLAYFS_UNIT}"
uninstall_systemd_unit "${SYSTEMD_CONTAINERD_UNIT}"
+ uninstall_systemd_unit "${SYSTEMD_STARGZ_UNIT}"
+ uninstall_systemd_unit "${SYSTEMD_IPFS_UNIT}"
+ uninstall_systemd_unit "${SYSTEMD_BYPASS4NETNSD_UNIT}"
INFO "This uninstallation tool does NOT remove containerd binaries and data."
INFO "To remove data, run: \`$BIN/rootlesskit rm -rf ${XDG_DATA_HOME}/containerd\`"
@@ -511,7 +527,10 @@ cmd_entrypoint_uninstall_buildkit() {
init
uninstall_systemd_unit "${SYSTEMD_BUILDKIT_UNIT}"
INFO "This uninstallation tool does NOT remove data."
- INFO "To remove data, run: \`$BIN/rootlesskit rm -rf ${XDG_DATA_HOME}/buildkit"
+ INFO "To remove data, run: \`$BIN/rootlesskit rm -rf ${XDG_DATA_HOME}/buildkit\`"
+ if [ -e "${XDG_CONFIG_HOME}/buildkit/buildkitd.toml" ]; then
+ INFO "You may also want to remove the daemon config: \`rm -f ${XDG_CONFIG_HOME}/buildkit/buildkitd.toml\`"
+ fi
}
# CLI subcommand: "uninstall-buildkit-containerd"
@@ -519,7 +538,7 @@ cmd_entrypoint_uninstall_buildkit_containerd() {
init
UNIT_NAME=${SYSTEMD_BUILDKIT_UNIT}
BUILDKIT_ROOT="${XDG_DATA_HOME}/buildkit"
- if [ -n "${CONTAINERD_NAMESPACE:-}" ] ; then
+ if [ -n "${CONTAINERD_NAMESPACE:-}" ]; then
UNIT_NAME="${CONTAINERD_NAMESPACE}-${SYSTEMD_BUILDKIT_UNIT}"
BUILDKIT_ROOT="${XDG_DATA_HOME}/buildkit-${CONTAINERD_NAMESPACE}"
fi
diff --git a/extras/rootless/containerd-rootless.sh b/extras/rootless/containerd-rootless.sh
index d394eeabe56..f569484a574 100755
--- a/extras/rootless/containerd-rootless.sh
+++ b/extras/rootless/containerd-rootless.sh
@@ -28,7 +28,7 @@
# External dependencies:
# * newuidmap and newgidmap needs to be installed.
# * /etc/subuid and /etc/subgid needs to be configured for the current user.
-# * RootlessKit (>= v0.10.0) needs to be installed. RootlessKit >= v0.14.1 is recommended.
+# * RootlessKit (>= v0.10.0) needs to be installed. RootlessKit >= v2.0.0 is recommended.
# * Either one of slirp4netns (>= v0.4.0), VPNKit, lxc-user-nic needs to be installed. slirp4netns >= v1.1.7 is recommended.
#
# Recognized environment variables:
@@ -38,27 +38,36 @@
# * CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=(builtin|slirp4netns): the rootlesskit port driver. Defaults to "builtin".
# * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX=(auto|true|false): whether to protect slirp4netns with a dedicated mount namespace. Defaults to "auto".
# * CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP=(auto|true|false): whether to protect slirp4netns with seccomp. Defaults to "auto".
+# * CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS=(auto|true|false): whether to launch rootlesskit with the "detach-netns" mode.
+# Defaults to "auto", which is resolved to "true" if RootlessKit >= 2.0 is installed.
+# The "detached-netns" mode accelerates `nerdctl (pull|push|build)` and enables `nerdctl run --net=host`,
+# however, there is a relatively minor drawback with BuildKit prior to v0.13:
+# the host loopback IP address (127.0.0.1) and abstract sockets are exposed to Dockerfile's "RUN" instructions during `nerdctl build` (not `nerdctl run`).
+# The drawback is fixed in BuildKit v0.13. Upgrading from a prior version of BuildKit needs removing the old systemd unit:
+# `containerd-rootless-setuptool.sh uninstall-buildkit && rm -f ~/.config/buildkit/buildkitd.toml`
+
+# See also: https://github.com/containerd/nerdctl/blob/main/docs/rootless.md#configuring-rootlesskit
set -e
-if ! [ -w $XDG_RUNTIME_DIR ]; then
+if ! [ -w "$XDG_RUNTIME_DIR" ]; then
echo "XDG_RUNTIME_DIR needs to be set and writable"
exit 1
fi
-if ! [ -w $HOME ]; then
+if ! [ -w "$HOME" ]; then
echo "HOME needs to be set and writable"
exit 1
fi
: "${XDG_DATA_HOME:=$HOME/.local/share}"
: "${XDG_CONFIG_HOME:=$HOME/.config}"
-if [ -z $_CONTAINERD_ROOTLESS_CHILD ]; then
+if [ -z "$_CONTAINERD_ROOTLESS_CHILD" ]; then
if [ "$(id -u)" = "0" ]; then
echo "Must not run as root"
exit 1
fi
case "$1" in
"check" | "install" | "uninstall")
- echo "Did you mean 'containerd-rootless-setuptool.sh $@' ?"
+ echo "Did you mean 'containerd-rootless-setuptool.sh $*' ?"
exit 1
;;
esac
@@ -69,21 +78,22 @@ if [ -z $_CONTAINERD_ROOTLESS_CHILD ]; then
: "${CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER:=builtin}"
: "${CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX:=auto}"
: "${CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP:=auto}"
+ : "${CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS:=auto}"
net=$CONTAINERD_ROOTLESS_ROOTLESSKIT_NET
mtu=$CONTAINERD_ROOTLESS_ROOTLESSKIT_MTU
- if [ -z $net ]; then
+ if [ -z "$net" ]; then
if command -v slirp4netns >/dev/null 2>&1; then
# If --netns-type is present in --help, slirp4netns is >= v0.4.0.
if slirp4netns --help | grep -qw -- --netns-type; then
net=slirp4netns
- if [ -z $mtu ]; then
+ if [ -z "$mtu" ]; then
mtu=65520
fi
else
echo "slirp4netns found but seems older than v0.4.0. Falling back to VPNKit."
fi
fi
- if [ -z $net ]; then
+ if [ -z "$net" ]; then
if command -v vpnkit >/dev/null 2>&1; then
net=vpnkit
else
@@ -92,7 +102,7 @@ if [ -z $_CONTAINERD_ROOTLESS_CHILD ]; then
fi
fi
fi
- if [ -z $mtu ]; then
+ if [ -z "$mtu" ]; then
mtu=1500
fi
@@ -107,6 +117,25 @@ if [ -z $_CONTAINERD_ROOTLESS_CHILD ]; then
export _CONTAINERD_ROOTLESS_SELINUX
fi
fi
+
+ case "$CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS" in
+ auto)
+ if rootlesskit --help | grep -qw -- "--detach-netns"; then
+ CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS="--detach-netns $CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS"
+ fi
+ ;;
+ 1 | true)
+ CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS="--detach-netns $CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS"
+ ;;
+ 0 | false)
+ # NOP
+ ;;
+ *)
+ echo "Unknown CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS value: $CONTAINERD_ROOTLESS_ROOTLESSKIT_DETACH_NETNS"
+ exit 1
+ ;;
+ esac
+
# Re-exec the script via RootlessKit, so as to create unprivileged {user,mount,network} namespaces.
#
# --copy-up allows removing/creating files in the directories by creating tmpfs and symlinks
@@ -115,18 +144,19 @@ if [ -z $_CONTAINERD_ROOTLESS_CHILD ]; then
# (by either systemd-networkd or NetworkManager)
# * /run: copy-up is required so that we can create /run/containerd (hardcoded) in our namespace
# * /var/lib: copy-up is required so that we can create /var/lib/containerd in our namespace
+ # shellcheck disable=SC2086
exec rootlesskit \
- --state-dir=$CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR \
- --net=$net --mtu=$mtu \
- --slirp4netns-sandbox=$CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX \
- --slirp4netns-seccomp=$CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP \
- --disable-host-loopback --port-driver=$CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER \
+ --state-dir="$CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR" \
+ --net="$net" --mtu="$mtu" \
+ --slirp4netns-sandbox="$CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SANDBOX" \
+ --slirp4netns-seccomp="$CONTAINERD_ROOTLESS_ROOTLESSKIT_SLIRP4NETNS_SECCOMP" \
+ --disable-host-loopback --port-driver="$CONTAINERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER" \
--copy-up=/etc --copy-up=/run --copy-up=/var/lib \
--propagation=rslave \
$CONTAINERD_ROOTLESS_ROOTLESSKIT_FLAGS \
- $0 $@
+ "$0" "$@"
else
- [ $_CONTAINERD_ROOTLESS_CHILD = 1 ]
+ [ "$_CONTAINERD_ROOTLESS_CHILD" = 1 ]
# Remove the *symlinks* for the existing files in the parent namespace if any,
# so that we can create our own files in our mount namespace.
# The actual files in the parent namespace are *not removed* by this rm command.
@@ -164,5 +194,5 @@ else
chcon system_u:object_r:iptables_var_run_t:s0 /run
fi
- exec containerd $@
+ exec containerd "$@"
fi
diff --git a/go.mod b/go.mod
index 0bc85eb2f89..c27a14d845a 100644
--- a/go.mod
+++ b/go.mod
@@ -1,132 +1,149 @@
-module github.com/containerd/nerdctl
+module github.com/containerd/nerdctl/v2
-go 1.19
+go 1.22.7
+
+// FIXME:
+// github.com/docker/docker/pkg/sysinfo has been replaced by a fork kept under ./pkg2/sysinfo
+// as Moby is not going to move to containerd v2 anytime soon or fix these transient dependencies.
+// We should still move back to upstream in the future, and remove our copy.
require (
- github.com/Masterminds/semver/v3 v3.2.1
- github.com/Microsoft/go-winio v0.6.1
- github.com/Microsoft/hcsshim v0.10.0-rc.8
- github.com/compose-spec/compose-go v1.14.0
- github.com/containerd/accelerated-container-image v0.6.7
- github.com/containerd/cgroups/v3 v3.0.1
- github.com/containerd/console v1.0.3
- github.com/containerd/containerd v1.7.2
- github.com/containerd/continuity v0.4.1
- github.com/containerd/go-cni v1.1.9
- github.com/containerd/imgcrypt v1.1.7
- github.com/containerd/nydus-snapshotter v0.9.0
- github.com/containerd/stargz-snapshotter v0.14.3
- github.com/containerd/stargz-snapshotter/estargz v0.14.3
- github.com/containerd/stargz-snapshotter/ipfs v0.14.3
- github.com/containerd/typeurl/v2 v2.1.1
- github.com/containernetworking/cni v1.1.2
- github.com/containernetworking/plugins v1.3.0
- github.com/coreos/go-iptables v0.6.0
+ github.com/Masterminds/semver/v3 v3.3.1
+ github.com/Microsoft/go-winio v0.6.2
+ github.com/Microsoft/hcsshim v0.12.9
+ github.com/compose-spec/compose-go/v2 v2.4.7
+ github.com/containerd/accelerated-container-image v1.2.3
+ github.com/containerd/cgroups/v3 v3.0.5
+ github.com/containerd/console v1.0.4
+ github.com/containerd/containerd/api v1.8.0
+ github.com/containerd/containerd/v2 v2.0.2
+ github.com/containerd/continuity v0.4.5
+ github.com/containerd/errdefs v1.0.0
+ github.com/containerd/fifo v1.1.0
+ github.com/containerd/go-cni v1.1.12
+ github.com/containerd/imgcrypt/v2 v2.0.0
+ github.com/containerd/log v0.1.0
+ github.com/containerd/nydus-snapshotter v0.15.0
+ github.com/containerd/platforms v1.0.0-rc.1
+ github.com/containerd/stargz-snapshotter v0.16.3
+ github.com/containerd/stargz-snapshotter/estargz v0.16.3
+ github.com/containerd/stargz-snapshotter/ipfs v0.16.3
+ github.com/containerd/typeurl/v2 v2.2.3
+ github.com/containernetworking/cni v1.2.3
+ github.com/containernetworking/plugins v1.5.1
+ github.com/coreos/go-iptables v0.8.0
github.com/coreos/go-systemd/v22 v22.5.0
- github.com/cyphar/filepath-securejoin v0.2.3
- github.com/docker/cli v24.0.2+incompatible
- github.com/docker/docker v24.0.2+incompatible
- github.com/docker/go-connections v0.4.0
+ github.com/cyphar/filepath-securejoin v0.4.0
+ github.com/distribution/reference v0.6.0
+ github.com/docker/cli v27.5.0+incompatible
+ github.com/docker/docker v27.5.0+incompatible
+ github.com/docker/go-connections v0.5.0
github.com/docker/go-units v0.5.0
- github.com/fahedouch/go-logrotate v0.1.3
- github.com/fatih/color v1.15.0
+ github.com/fahedouch/go-logrotate v0.2.1
+ github.com/fatih/color v1.18.0
github.com/fluent/fluent-logger-golang v1.9.0
- github.com/hashicorp/go-multierror v1.1.1
+ github.com/fsnotify/fsnotify v1.8.0
+ github.com/go-viper/mapstructure/v2 v2.2.1
github.com/ipfs/go-cid v0.4.1
- github.com/mattn/go-isatty v0.0.19
- github.com/mitchellh/mapstructure v1.5.0
- github.com/moby/sys/mount v0.3.3
- github.com/moby/sys/signal v0.7.0
- github.com/moby/term v0.5.0
+ github.com/klauspost/compress v1.17.11
+ github.com/mattn/go-isatty v0.0.20
+ github.com/moby/sys/mount v0.3.4
+ github.com/moby/sys/mountinfo v0.7.2
+ github.com/moby/sys/signal v0.7.1
+ github.com/moby/sys/userns v0.1.0
+ github.com/moby/term v0.5.2
+ github.com/muesli/cancelreader v0.2.2
github.com/opencontainers/go-digest v1.0.0
- github.com/opencontainers/image-spec v1.1.0-rc3
- github.com/opencontainers/runtime-spec v1.1.0-rc.3
- github.com/pelletier/go-toml v1.9.5
- github.com/rootless-containers/bypass4netns v0.3.0
- github.com/rootless-containers/rootlesskit v1.1.1
- github.com/sirupsen/logrus v1.9.3
- github.com/spf13/cobra v1.7.0
+ github.com/opencontainers/image-spec v1.1.0
+ github.com/opencontainers/runtime-spec v1.2.0
+ github.com/pelletier/go-toml/v2 v2.2.3
+ github.com/rootless-containers/bypass4netns v0.4.2
+ github.com/rootless-containers/rootlesskit/v2 v2.3.2
+ github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
- github.com/tidwall/gjson v1.14.4
- github.com/vishvananda/netlink v1.2.1-beta.2
- github.com/vishvananda/netns v0.0.4
+ github.com/vishvananda/netlink v1.3.0
+ github.com/vishvananda/netns v0.0.5
github.com/yuchanns/srslog v1.1.0
- golang.org/x/crypto v0.10.0
- golang.org/x/net v0.11.0
- golang.org/x/sync v0.3.0
- golang.org/x/sys v0.9.0
- golang.org/x/term v0.9.0
- golang.org/x/text v0.10.0
+ go.uber.org/mock v0.5.0
+ golang.org/x/crypto v0.32.0
+ golang.org/x/net v0.34.0
+ golang.org/x/sync v0.10.0
+ golang.org/x/sys v0.29.0
+ golang.org/x/term v0.28.0
+ golang.org/x/text v0.21.0
gopkg.in/yaml.v3 v3.0.1
- gotest.tools/v3 v3.4.0
+ gotest.tools/v3 v3.5.1
)
require (
- github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect
- github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect
- github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
- github.com/cilium/ebpf v0.9.1 // indirect
- github.com/containerd/cgroups v1.1.0 // indirect
- github.com/containerd/fifo v1.1.0 // indirect
- github.com/containerd/ttrpc v1.2.2 // indirect
- github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 // indirect
- github.com/containers/ocicrypt v1.1.7 // indirect
- github.com/distribution/distribution/v3 v3.0.0-20230214150026-36d8c594d7aa // indirect
- github.com/djherbis/times v1.5.0 // indirect
- github.com/docker/docker-credential-helpers v0.7.0 // indirect
- github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
- github.com/frankban/quicktest v1.14.2 // indirect
- github.com/go-logr/logr v1.2.4 // indirect
+ github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect
+ github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
+ github.com/cilium/ebpf v0.16.0 // indirect
+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
+ github.com/containerd/go-runc v1.1.0 // indirect
+ github.com/containerd/plugin v1.0.0 // indirect
+ github.com/containerd/ttrpc v1.2.7 // indirect
+ github.com/containers/ocicrypt v1.2.1 // indirect
+ github.com/djherbis/times v1.6.0 // indirect
+ github.com/docker/docker-credential-helpers v0.8.2 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/go-jose/go-jose/v4 v4.0.4 // indirect
+ github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/go-cmp v0.5.9 // indirect
- github.com/google/uuid v1.3.0 // indirect
- github.com/hashicorp/errwrap v1.1.0 // indirect
- github.com/imdario/mergo v0.3.15 // indirect
+ github.com/golang/protobuf v1.5.4 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
- github.com/klauspost/compress v1.16.6
- github.com/klauspost/cpuid/v2 v2.1.1 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-shellwords v1.0.12 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
- github.com/minio/sha256-simd v1.0.0 // indirect
+ github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
- github.com/moby/sys/mountinfo v0.6.2 // indirect
- github.com/moby/sys/sequential v0.5.0 // indirect
- github.com/moby/sys/symlink v0.2.0 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/symlink v0.3.0 // indirect
+ github.com/moby/sys/user v0.3.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
- github.com/multiformats/go-base36 v0.1.0 // indirect
- github.com/multiformats/go-multiaddr v0.8.0 // indirect
- github.com/multiformats/go-multibase v0.1.1 // indirect
- github.com/multiformats/go-multihash v0.2.1 // indirect
- github.com/multiformats/go-varint v0.0.6 // indirect
- github.com/opencontainers/runc v1.1.7 // indirect
- github.com/opencontainers/selinux v1.11.0 // indirect
- github.com/philhofer/fwd v1.1.1 // indirect
+ github.com/multiformats/go-base36 v0.2.0 // indirect
+ github.com/multiformats/go-multiaddr v0.13.0 // indirect
+ github.com/multiformats/go-multibase v0.2.0 // indirect
+ github.com/multiformats/go-multihash v0.2.3 // indirect
+ github.com/multiformats/go-varint v0.0.7 // indirect
+ github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 // indirect
+ github.com/opencontainers/selinux v1.11.1 // indirect
+ github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect
+ github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
github.com/pkg/errors v0.9.1 // indirect
+ github.com/sasha-s/go-deadlock v0.3.5 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/smallstep/pkcs7 v0.1.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
- github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 // indirect
- github.com/tidwall/match v1.1.1 // indirect
- github.com/tidwall/pretty v1.2.0 // indirect
- github.com/tinylib/msgp v1.1.6 // indirect
- github.com/vbatts/tar-split v0.11.2 // indirect
+ github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect
+ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect
+ github.com/tinylib/msgp v1.2.0 // indirect
+ github.com/vbatts/tar-split v0.11.6 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
- go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 // indirect
go.opencensus.io v0.24.0 // indirect
- go.opentelemetry.io/otel v1.14.0 // indirect
- go.opentelemetry.io/otel/trace v1.14.0 // indirect
- golang.org/x/mod v0.9.0 // indirect
- golang.org/x/tools v0.7.0 // indirect
- google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
- google.golang.org/grpc v1.54.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
- gopkg.in/square/go-jose.v2 v2.5.1 // indirect
- lukechampine.com/blake3 v1.1.7 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
+ go.opentelemetry.io/otel v1.31.0 // indirect
+ go.opentelemetry.io/otel/metric v1.31.0 // indirect
+ go.opentelemetry.io/otel/trace v1.31.0 // indirect
+ golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect
+ golang.org/x/mod v0.22.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
+ google.golang.org/grpc v1.69.4 // indirect
+ google.golang.org/protobuf v1.36.2 // indirect
+ lukechampine.com/blake3 v1.3.0 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
+ tags.cncf.io/container-device-interface v0.8.0 // indirect
+ tags.cncf.io/container-device-interface/specs-go v0.8.0 // indirect
)
diff --git a/go.sum b/go.sum
index 5eeffc995b1..39fe1cf07b5 100644
--- a/go.sum
+++ b/go.sum
@@ -1,1532 +1,487 @@
-bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
-bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
-cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
-cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
-cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
-cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
-cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
-cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
-cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
-cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
-cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
-cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
-cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
-cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
-cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
-cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
-cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
-cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
-cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
-cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
-cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
-cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
-cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
-github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
-github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 h1:+vTEFqeoeur6XSq06bs+roX3YiT49gUniJK7Zky7Xjg=
-github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=
-github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
-github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
-github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
-github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
-github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
-github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
-github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
-github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
-github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
-github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
-github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
-github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
-github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
-github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
-github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
-github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
+github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M=
+github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
+github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
-github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
-github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
-github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
-github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
-github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
-github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
-github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
-github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
-github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
-github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
-github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ=
-github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
-github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg=
-github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00=
-github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600=
-github.com/Microsoft/hcsshim v0.8.20/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
-github.com/Microsoft/hcsshim v0.8.21/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4=
-github.com/Microsoft/hcsshim v0.8.23/go.mod h1:4zegtUJth7lAvFyc6cH2gGQ5B3OFQim01nnU2M8jKDg=
-github.com/Microsoft/hcsshim v0.9.2/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
-github.com/Microsoft/hcsshim v0.9.4/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc=
-github.com/Microsoft/hcsshim v0.10.0-rc.8 h1:YSZVvlIIDD1UxQpJp0h+dnpLUw+TrY0cx8obKsp3bek=
-github.com/Microsoft/hcsshim v0.10.0-rc.8/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
-github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU=
-github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY=
-github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
-github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
-github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
-github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
-github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
-github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0=
-github.com/alexflint/go-filemutex v1.1.0/go.mod h1:7P4iRhttt/nUvUOrYIhcpMzv2G6CY9UnI16Z+UJqRyk=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
-github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
-github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
-github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
-github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
-github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
+github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
+github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
+github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg=
+github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
-github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
-github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
-github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
-github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
-github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
-github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
-github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
-github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
-github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
-github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
-github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
-github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
-github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
-github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
-github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
-github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4=
-github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY=
+github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
+github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
-github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo=
-github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA=
-github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI=
-github.com/compose-spec/compose-go v1.14.0 h1:/+tQxBEPIrfsi87Qh7/VjMzcJN3BRNER/RO71ku+u6E=
-github.com/compose-spec/compose-go v1.14.0/go.mod h1:m0o4G6MQDHjjz9rY7No9FpnNi+9sKic262rzrwuCqic=
-github.com/containerd/accelerated-container-image v0.6.7 h1:QDO12lgUubiUq0ogMzcL6CdSxkzFOX7vVaSIXAJ9EaM=
-github.com/containerd/accelerated-container-image v0.6.7/go.mod h1:a7MYTlNhR4+GGpXD7wuNSgxrwC2wE2rgUfCvef+FQzg=
-github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE=
-github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU=
-github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
-github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU=
-github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E=
-github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
-github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss=
-github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
-github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
-github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
-github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
-github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo=
-github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
-github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU=
-github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8=
-github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
-github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
-github.com/containerd/cgroups/v3 v3.0.1 h1:4hfGvu8rfGIwVIDd+nLzn/B9ZXx4BcCjzt5ToenJRaE=
-github.com/containerd/cgroups/v3 v3.0.1/go.mod h1:/vtwk1VXrtoa5AaZLkypuOJgA/6DyPMZHJPGQNtlHnw=
-github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
-github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
-github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
-github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
-github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
-github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
-github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
-github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.4.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
-github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ=
-github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU=
-github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI=
-github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s=
-github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g=
-github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c=
-github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s=
-github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE=
-github.com/containerd/containerd v1.6.8/go.mod h1:By6p5KqPK0/7/CgO/A6t/Gz+CUYUu2zf1hUaaymVXB0=
-github.com/containerd/containerd v1.7.2 h1:UF2gdONnxO8I6byZXDi5sXWiWvlW3D/sci7dTQimEJo=
-github.com/containerd/containerd v1.7.2/go.mod h1:afcz74+K10M/+cjGHIVQrCt3RAQhUSCAjJ9iMYhhkuI=
-github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
-github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
-github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
-github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
-github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y=
-github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ=
-github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM=
-github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk=
-github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU=
-github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
-github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
-github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
-github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
-github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
-github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
-github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4=
+github.com/compose-spec/compose-go/v2 v2.4.7 h1:WNpz5bIbKG+G+w9pfu72B1ZXr+Og9jez8TMEo8ecXPk=
+github.com/compose-spec/compose-go/v2 v2.4.7/go.mod h1:lFN0DrMxIncJGYAXTfWuajfwj5haBJqrBkarHcnjJKc=
+github.com/containerd/accelerated-container-image v1.2.3 h1:tAIoP7Z7b2xGhb7QCM5Fa+2xqWfPqRmyi5lodbsGGRA=
+github.com/containerd/accelerated-container-image v1.2.3/go.mod h1:EvKVWor6ZQNUyYp0MZm5hw4k21ropuz7EegM+m/Jb/Q=
+github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo=
+github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins=
+github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
+github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
+github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
+github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
+github.com/containerd/containerd/v2 v2.0.2 h1:GmH/tRBlTvrXOLwSpWE2vNAm8+MqI6nmxKpKBNKY8Wc=
+github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI=
+github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=
+github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=
github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=
-github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU=
-github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk=
-github.com/containerd/go-cni v1.1.0/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=
-github.com/containerd/go-cni v1.1.3/go.mod h1:Rflh2EJ/++BA2/vY5ao3K6WJRR/bZKsX123aPk+kUtA=
-github.com/containerd/go-cni v1.1.6/go.mod h1:BWtoWl5ghVymxu6MBjg79W9NZrCRyHIdUtk4cauMe34=
-github.com/containerd/go-cni v1.1.9 h1:ORi7P1dYzCwVM6XPN4n3CbkuOx/NZ2DOqy+SHRdo9rU=
-github.com/containerd/go-cni v1.1.9/go.mod h1:XYrZJ1d5W6E2VOvjffL3IZq0Dz6bsVlERHbekNK90PM=
-github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
-github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
-github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
-github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
-github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok=
-github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0=
-github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA=
-github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow=
-github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms=
-github.com/containerd/imgcrypt v1.1.3/go.mod h1:/TPA1GIDXMzbj01yd8pIbQiLdQxed5ue1wb8bP7PQu4=
-github.com/containerd/imgcrypt v1.1.4/go.mod h1:LorQnPtzL/T0IyCeftcsMEO7AqxUDbdO8j/tSUpgxvo=
-github.com/containerd/imgcrypt v1.1.7 h1:WSf9o9EQ0KGHiUx2ESFZ+PKf4nxK9BcvV/nJDX8RkB4=
-github.com/containerd/imgcrypt v1.1.7/go.mod h1:FD8gqIcX5aTotCtOmjeCsi3A1dHmTZpnMISGKSczt4k=
-github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c=
-github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
-github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY=
-github.com/containerd/nydus-snapshotter v0.9.0 h1:f0Tr3srVKDlURgLG/Kocy4WQIYsmSoc8ihHxdzfB2S0=
-github.com/containerd/nydus-snapshotter v0.9.0/go.mod h1:xEsAzeM0gZEW6POBPOa+1X7EThYsEJNWnO/fhf2moYU=
-github.com/containerd/stargz-snapshotter v0.14.3 h1:OTUVZoPSPs8mGgmQUE1dqw3WX/3nrsmsurW7UPLWl1U=
-github.com/containerd/stargz-snapshotter v0.14.3/go.mod h1:j2Ya4JeA5gMZJr8BchSkPjlcCEh++auAxp4nidPI6N0=
-github.com/containerd/stargz-snapshotter/estargz v0.4.1/go.mod h1:x7Q9dg9QYb4+ELgxmo4gBUeJB0tl5dqH1Sdz0nJU1QM=
-github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k=
-github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o=
-github.com/containerd/stargz-snapshotter/ipfs v0.14.3 h1:Y9jAdsjvZyG30dnEUvfMyV0FOnfs/pdEuYcxYMTDGSM=
-github.com/containerd/stargz-snapshotter/ipfs v0.14.3/go.mod h1:Y7oQmTVPao0mE8S6WqIKVTnd59FNW55Gy0r/RIGySew=
-github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
-github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
-github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8=
-github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
-github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
-github.com/containerd/ttrpc v1.1.0/go.mod h1:XX4ZTnoOId4HklF4edwc4DcqskFZuvXB1Evzy5KFQpQ=
-github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs=
-github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak=
-github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
-github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk=
-github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
-github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s=
-github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 h1:rQvjv7gRi6Ki/NS/U9oLZFhqyk4dh/GH2M3o/4BRkMM=
-github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67/go.mod h1:HDkcKOXRnX6yKnXv3P0QrogFi0DoiauK/LpQi961f0A=
-github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4=
-github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0=
-github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw=
-github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y=
-github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
-github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
-github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY=
-github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
-github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
-github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
-github.com/containernetworking/cni v1.0.1/go.mod h1:AKuhXbN5EzmD4yTNtfSsX3tPcmtrBI6QcRV0NiNt15Y=
-github.com/containernetworking/cni v1.1.1/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
-github.com/containernetworking/cni v1.1.2 h1:wtRGZVv7olUHMOqouPpn3cXJWpJgM6+EUl31EQbXALQ=
-github.com/containernetworking/cni v1.1.2/go.mod h1:sDpYKmGVENF3s6uvMvGgldDWeG8dMxakj/u+i9ht9vw=
-github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM=
-github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8=
-github.com/containernetworking/plugins v1.0.1/go.mod h1:QHCfGpaTwYTbbH+nZXKVTxNBDZcxSOplJT5ico8/FLE=
-github.com/containernetworking/plugins v1.1.1/go.mod h1:Sr5TH/eBsGLXK/h71HeLfX19sZPp3ry5uHSkI4LPxV8=
-github.com/containernetworking/plugins v1.3.0 h1:QVNXMT6XloyMUoO2wUOqWTC1hWFV62Q6mVDp5H1HnjM=
-github.com/containernetworking/plugins v1.3.0/go.mod h1:Pc2wcedTQQCVuROOOaLBPPxrEXqqXBFt3cZ+/yVg6l0=
-github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc=
-github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4=
-github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
-github.com/containers/ocicrypt v1.1.2/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
-github.com/containers/ocicrypt v1.1.3/go.mod h1:xpdkbVAuaH3WzbEabUd5yDsl9SwJA5pABH85425Es2g=
-github.com/containers/ocicrypt v1.1.6/go.mod h1:WgjxPWdTJMqYMjf3M6cuIFFA1/MpyyhIM99YInA+Rvc=
-github.com/containers/ocicrypt v1.1.7 h1:thhNr4fu2ltyGz8aMx8u48Ae0Pnbip3ePP9/mzkZ/3U=
-github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8NscCYRawuDNtw=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
-github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
-github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk=
-github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
-github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
-github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
-github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
-github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/containerd/go-cni v1.1.12 h1:wm/5VD/i255hjM4uIZjBRiEQ7y98W9ACy/mHeLi4+94=
+github.com/containerd/go-cni v1.1.12/go.mod h1:+jaqRBdtW5faJxj2Qwg1Of7GsV66xcvnCx4mSJtUlxU=
+github.com/containerd/go-runc v1.1.0 h1:OX4f+/i2y5sUT7LhmcJH7GYrjjhHa1QI4e8yO0gGleA=
+github.com/containerd/go-runc v1.1.0/go.mod h1:xJv2hFF7GvHtTJd9JqTS2UVxMkULUYw4JN5XAUZqH5U=
+github.com/containerd/imgcrypt/v2 v2.0.0 h1:vd2ByN6cXeearzXCQljH1eYe77FgFO5/B9+dK14mng0=
+github.com/containerd/imgcrypt/v2 v2.0.0/go.mod h1:S4kOVvPZRerVueZULagcwkJK7sKc/wQI/ixcmyj26uY=
+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
+github.com/containerd/nydus-snapshotter v0.15.0 h1:RqZRs1GPeM6T3wmuxJV9u+2Rg4YETVMwTmiDeX+iWC8=
+github.com/containerd/nydus-snapshotter v0.15.0/go.mod h1:biq0ijpeZe0I5yZFSJyHzFSjjRZQ7P7y/OuHyd7hYOw=
+github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
+github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
+github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=
+github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=
+github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4=
+github.com/containerd/stargz-snapshotter v0.16.3/go.mod h1:XPOl2oa9zjWidTM2IX191smolwWc3/zkKtp02TzTFb0=
+github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
+github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
+github.com/containerd/stargz-snapshotter/ipfs v0.16.3 h1:d6IBSzYo0vlFcujwTqJRwpI3cZgX3E2I6Ev7LtMaZ4M=
+github.com/containerd/stargz-snapshotter/ipfs v0.16.3/go.mod h1:d4EuGnC3RteInKAdddUbDOL88uw3vZySSLZ44pbriGM=
+github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=
+github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=
+github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
+github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
+github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM=
+github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M=
+github.com/containernetworking/plugins v1.5.1 h1:T5ji+LPYjjgW0QM+KyrigZbLsZ8jaX+E5J/EcKOE4gQ=
+github.com/containernetworking/plugins v1.5.1/go.mod h1:MIQfgMayGuHYs0XdNudf31cLLAC+i242hNm6KuDGqCM=
+github.com/containers/ocicrypt v1.2.1 h1:0qIOTT9DoYwcKmxSt8QJt+VzMY18onl9jUXsxpVhSmM=
+github.com/containers/ocicrypt v1.2.1/go.mod h1:aD0AAqfMp0MtwqWgHM1bUwe1anx0VazI108CRrSKINQ=
+github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
+github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
-github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
-github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
-github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
-github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
-github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
-github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ=
-github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s=
-github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
-github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/cyphar/filepath-securejoin v0.4.0 h1:PioTG9TBRSApBpYGnDU8HC+miIsX8vitBH9LGNNMoLQ=
+github.com/cyphar/filepath-securejoin v0.4.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
-github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/distribution/distribution/v3 v3.0.0-20230214150026-36d8c594d7aa h1:L9Ay/slwQ4ERSPaurC+TVkZrM0K98GNrEEo1En3e8as=
-github.com/distribution/distribution/v3 v3.0.0-20230214150026-36d8c594d7aa/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI=
-github.com/djherbis/times v1.5.0 h1:79myA211VwPhFTqUk8xehWrsEO+zcIZj0zT8mXPVARU=
-github.com/djherbis/times v1.5.0/go.mod h1:5q7FDLvbNg1L/KaBmPcWlVR9NmoKo3+ucqUA3ijQhA0=
-github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
-github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/cli v24.0.2+incompatible h1:QdqR7znue1mtkXIJ+ruQMGQhpw2JzMJLRXp6zpzF6tM=
-github.com/docker/cli v24.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
-github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg=
-github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
-github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
-github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A=
-github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0=
-github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
-github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
-github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
-github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
-github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
-github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI=
-github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=
-github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
+github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
+github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
+github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
+github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U=
+github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
+github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
+github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
+github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
-github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE=
-github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
-github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
-github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
-github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
-github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
-github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/fahedouch/go-logrotate v0.1.3 h1:V5VGDXfKjzjuISflMsxJqKsHggpZE/7iNyxfyNlu5A8=
-github.com/fahedouch/go-logrotate v0.1.3/go.mod h1:S8a52JNmJe9t7XdO5Y3NZ36Va3HcVQrI/HBJLpvnc9w=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
-github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
-github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/fahedouch/go-logrotate v0.2.1 h1:Q0Hk9Kp/Y4iwy9uR9e/60fEoxGhvfk8MG7WwtL9aarM=
+github.com/fahedouch/go-logrotate v0.2.1/go.mod h1:Mmyex1f9fGXBNnhS9uHsbnO9BGvADF4VGqVnqAJalgc=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fluent/fluent-logger-golang v1.9.0 h1:zUdY44CHX2oIUc7VTNZc+4m+ORuO/mldQDA7czhWXEg=
github.com/fluent/fluent-logger-golang v1.9.0/go.mod h1:2/HCT/jTy78yGyeNGQLGQsjF3zzzAuy6Xlk6FCMV5eU=
-github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
-github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
-github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
-github.com/frankban/quicktest v1.14.2 h1:SPb1KFFmM+ybpEjPUhCCkZOM5xlovT5UbrMvWnXyBns=
-github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
-github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
-github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
-github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
-github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
-github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
-github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
-github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
+github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
-github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
-github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
-github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
-github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
-github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
-github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
-github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
-github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
-github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
-github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
-github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
-github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
-github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
-github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
-github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
-github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
+github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
-github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
-github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
-github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
-github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
-github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
-github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
-github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0=
-github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
-github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
-github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
-github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=
-github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
-github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
-github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
-github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
-github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
-github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
-github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ=
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
-github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
-github.com/j-keck/arping v1.0.2/go.mod h1:aJbELhR92bSk7tp79AWM/ftfc90EfEi2bQJrbBFOsPw=
-github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
-github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
-github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
+github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM=
+github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
-github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.1.1 h1:t0wUqjowdm8ezddV5k0tLWVklVuvLJpoHeb4WBdydm0=
-github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
-github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
-github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
-github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
-github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
-github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
-github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
+github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
-github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
-github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
-github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
+github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
+github.com/mndrix/tap-go v0.0.0-20171203230836-629fa407e90b/go.mod h1:pzzDgJWZ34fGzaAZGFW22KVZDfyrYW+QABMrWnJBnSs=
+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
-github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
-github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs=
-github.com/moby/sys/mount v0.3.3/go.mod h1:PBaEorSNTLG5t/+4EgukEQVlAvVEc6ZjTySwKdqp5K0=
-github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
-github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
-github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU=
-github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78=
-github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
-github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
-github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
-github.com/moby/sys/signal v0.6.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
-github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=
-github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=
-github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ=
-github.com/moby/sys/symlink v0.2.0 h1:tk1rOM+Ljp0nFmfOIBtlV3rTDlWOwFRhjEeAhZB0nZc=
-github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs=
-github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo=
-github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A=
-github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
-github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
-github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
-github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww=
+github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os=
+github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
+github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
+github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=
+github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=
+github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU=
+github.com/moby/sys/symlink v0.3.0/go.mod h1:3eNdhduHmYPcgsJtZXW1W4XUJdZGBIkttZ8xKqPUJq0=
+github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=
+github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
+github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
+github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
-github.com/multiformats/go-base36 v0.1.0 h1:JR6TyF7JjGd3m6FbLU2cOxhC0Li8z8dLNGQ89tUg4F4=
-github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
-github.com/multiformats/go-multiaddr v0.8.0 h1:aqjksEcqK+iD/Foe1RRFsGZh8+XFiGo7FgUCZlpv3LU=
-github.com/multiformats/go-multiaddr v0.8.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs=
-github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI=
-github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8=
-github.com/multiformats/go-multihash v0.2.1 h1:aem8ZT0VA2nCHHk7bPJ1BjUbHNciqZC/d16Vve9l108=
-github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc=
-github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
-github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
-github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
-github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
-github.com/networkplumbing/go-nft v0.2.0/go.mod h1:HnnM+tYvlGAsMU7yoYwXEVLLiDW9gdMmb5HoGcwpuQs=
-github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
-github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
-github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
-github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
-github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
-github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
-github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
-github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
-github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
-github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU=
-github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
-github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
-github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
-github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
-github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
-github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
-github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
-github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
-github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
+github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
+github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
+github.com/multiformats/go-multiaddr v0.13.0 h1:BCBzs61E3AGHcYYTv8dqRH43ZfyrqM8RXVPT8t13tLQ=
+github.com/multiformats/go-multiaddr v0.13.0/go.mod h1:sBXrNzucqkFJhvKOiwwLyqamGa/P5EIXNPLovyhQCII=
+github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
+github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
+github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
+github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
+github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
+github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
-github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
-github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8=
-github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
-github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
-github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
-github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
-github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
-github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0=
-github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
-github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=
-github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=
-github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk=
-github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50=
-github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-spec v1.1.0-rc.3 h1:l04uafi6kxByhbxev7OWiuUv0LZxEsYUfDWZ6bztAuU=
-github.com/opencontainers/runtime-spec v1.1.0-rc.3/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
-github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
-github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
-github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
-github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
-github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
-github.com/opencontainers/selinux v1.10.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
-github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU=
-github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
-github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
-github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
-github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
-github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ=
-github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
+github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
+github.com/opencontainers/runtime-spec v1.0.3-0.20220825212826-86290f6a00fb/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
+github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0=
+github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI=
+github.com/opencontainers/selinux v1.9.1/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
+github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8=
+github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw=
+github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
+github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 h1:jYi87L8j62qkXzaYHAQAhEapgukhenIMZRBKTNRLHJ4=
+github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
-github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
-github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
-github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
-github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
-github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
-github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
-github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
-github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
-github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
-github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
-github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
-github.com/rootless-containers/bypass4netns v0.3.0 h1:UwI55zWDZz7OGyN4YWgfCKdsI58VGY7OlghcLdxJX10=
-github.com/rootless-containers/bypass4netns v0.3.0/go.mod h1:IXHPjkQlJRygNBCN0hSSR3ITX6kDKr3aAaGHx6APd+g=
-github.com/rootless-containers/rootlesskit v1.1.1 h1:F5psKWoWY9/VjZ3ifVcaosjvFZJOagX85U22M0/EQZE=
-github.com/rootless-containers/rootlesskit v1.1.1/go.mod h1:UD5GoA3dqKCJrnvnhVgQQnweMF2qZnf9KLw8EewcMZI=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
+github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
+github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
+github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
+github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc=
+github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk=
+github.com/rootless-containers/rootlesskit/v2 v2.3.2 h1:QZk7sKU3+B8UHretEeIg6NSTTpj0o4iHGNhNbJBnHOU=
+github.com/rootless-containers/rootlesskit/v2 v2.3.2/go.mod h1:RL7YzL02nA2d8HAzt5d1nZnuiAeudQ4oym+HF/7sk7U=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
-github.com/safchain/ethtool v0.0.0-20210803160452-9aa261dae9b1/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4=
-github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
-github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
-github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
-github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
-github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
-github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU=
+github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/smallstep/pkcs7 v0.1.1 h1:x+rPdt2W088V9Vkjho4KtoggyktZJlMduZAtRHm68LU=
+github.com/smallstep/pkcs7 v0.1.1/go.mod h1:dL6j5AIz9GHjVEBTXtW+QliALcgM19RtXaTeyxI+AfA=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
-github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
-github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
-github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
-github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
-github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980 h1:lIOOHPEbXzO3vnmx2gok1Tfs31Q8GQqKLc8vVqyQq/I=
-github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
-github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
-github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw=
+github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
-github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
-github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
-github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
-github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
-github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
-github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
-github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
-github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
-github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
-github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM=
-github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
-github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-github.com/vbatts/tar-split v0.11.2 h1:Via6XqJr0hceW4wff3QRzD5gAk/tatMw/4ZA7cTlIME=
-github.com/vbatts/tar-split v0.11.2/go.mod h1:vV3ZuO2yWSVsz+pfFzDG/upWH1JhjOiEaWq6kXyQ3VI=
-github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
-github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
-github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netlink v1.1.1-0.20210330154013-f5de75959ad5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
-github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
-github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
-github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
-github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
-github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
-github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/tinylib/msgp v1.2.0 h1:0uKB/662twsVBpYUPbokj4sTSKhWFKB7LopO2kWK8lY=
+github.com/tinylib/msgp v1.2.0/go.mod h1:2vIGs3lcUo8izAATNobrCHevYZC/LMsJtw4JPiYPHro=
+github.com/urfave/cli v1.19.1/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
+github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
+github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
+github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk=
+github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
-github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
-github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
+github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
+github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
-github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk=
github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak=
-github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
-github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
-github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
-go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
-go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg=
-go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
-go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
-go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
-go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
-go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE=
-go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc=
-go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4=
-go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M=
-go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
-go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
-go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0/go.mod h1:vEhqr0m4eTc+DWxfsXoXue2GBgV2uUwVznkGIHW/e5w=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
-go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
-go.opentelemetry.io/otel v1.3.0/go.mod h1:PWIKzi6JCp7sM0k9yZ43VX+T345uNbAkDKwHVjb2PTs=
-go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM=
-go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU=
-go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0/go.mod h1:keUU7UfnwWTWpJ+FWnyqmogPa82nuU5VUANFq49hlMY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE=
-go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
-go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
-go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
-go.opentelemetry.io/otel/sdk v1.3.0/go.mod h1:rIo4suHNhQwBIPg9axF8V9CA72Wz2mKF1teNrup8yzs=
-go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
-go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=
-go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
-go.opentelemetry.io/otel/trace v1.3.0/go.mod h1:c/VDhno8888bvQYmbYLqe41/Ldmr/KKunbvWM4/fEjk=
-go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M=
-go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ=
-go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
+go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY=
+go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE=
+go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
+go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
+go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
+go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=
+go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=
+go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=
+go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys=
+go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
-golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
+go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
-golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
+golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
-golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
-golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo=
+golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
-golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
-golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
-golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
+golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
-golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
-golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
-golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
+golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
-golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
-golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
-golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
+golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
+golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
-golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
-golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
-golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
-golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
-golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
-golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
-golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
-golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o=
+golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
-google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
-google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
-google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
-google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
-google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
-google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
-google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
-google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
-google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
-google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
-google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
-google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
-google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
-google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
-google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
-google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
-google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
-google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
-google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
+google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A=
+google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1535,117 +490,25 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU=
+google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
-gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
-gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
-gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
-gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
-gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
-gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
-k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
-k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
-k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs=
-k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
-k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
-k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
-k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
-k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U=
-k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
-k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
-k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
-k8s.io/apiserver v0.22.5/go.mod h1:s2WbtgZAkTKt679sYtSudEQrTGWUSQAPe6MupLnlmaQ=
-k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
-k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k=
-k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0=
-k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y=
-k8s.io/code-generator v0.19.7/go.mod h1:lwEq3YnLYb/7uVXLorOJfxg+cUu2oihFhHZ0n9NIla0=
-k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
-k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
-k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=
-k8s.io/component-base v0.22.5/go.mod h1:VK3I+TjuF9eaa+Ln67dKxhGar5ynVbwnGrUiNF4MqCI=
-k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM=
-k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
-k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI=
-k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc=
-k8s.io/cri-api v0.23.1/go.mod h1:REJE3PSU0h/LOV1APBrupxrEJqnoxZC8KWzkBUHwrK4=
-k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
-k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
-k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
-k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
-k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
-k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
-k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o=
-k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
-k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
-k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
-k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
-k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
-lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
-lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
-rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
-rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
-sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
-sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
-sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
+lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
+lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
+tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc=
+tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y=
+tags.cncf.io/container-device-interface/specs-go v0.8.0 h1:QYGFzGxvYK/ZLMrjhvY0RjpUavIn4KcmRmVP/JjdBTA=
+tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws=
diff --git a/hack/build-integration-canary.sh b/hack/build-integration-canary.sh
new file mode 100755
index 00000000000..0396ada100e
--- /dev/null
+++ b/hack/build-integration-canary.sh
@@ -0,0 +1,348 @@
+#!/usr/bin/env bash
+
+# Copyright The containerd Authors.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
+readonly root
+# shellcheck source=/dev/null
+. "$root/scripts/lib.sh"
+
+######################
+# Definitions
+######################
+
+# "Blacklisting" here means that any dependency which name is blacklisted will be left untouched, at the version
+# currently pinned in the Dockerfile.
+# This is convenient so that currently broken alpha/beta/RC can be held back temporarily to keep the build green
+blacklist=()
+
+# List all the repositories we depend on to build and run integration tests
+dependencies=(
+ ktock/buildg
+ moby/buildkit
+ containerd/containerd
+ distribution/distribution
+ containers/fuse-overlayfs
+ containerd/fuse-overlayfs-snapshotter
+ gotestyourself/gotestsum
+ ipfs/kubo
+ containerd/nydus-snapshotter
+ containernetworking/plugins
+ rootless-containers/rootlesskit
+ opencontainers/runc
+ rootless-containers/slirp4netns
+ awslabs/soci-snapshotter
+ containerd/stargz-snapshotter
+ krallin/tini
+)
+
+# Certain dependencies do issue multiple unrelated releaes on their repo - use these below to ignore certain releases
+BUILDKIT_EXCLUDE="dockerfile/"
+CONTAINERD_EXCLUDE="containerd API"
+
+# Some dependencies will be checksum-matched. Setting the variables below will trigger us to download and generate shasums
+# The value you set the variable to also decides which artifacts you are interested in.
+BUILDKIT_CHECKSUM=linux
+CNI_PLUGINS_CHECKSUM=linux
+CONTAINERD_FUSE_OVERLAYFS_CHECKSUM=linux
+FUSE_OVERLAYFS_CHECKSUM=linux
+# Avoids the full build
+BUILDG_CHECKSUM=buildg-v
+ROOTLESSKIT_CHECKSUM=linux
+SLIRP4NETNS_CHECKSUM=linux
+STARGZ_SNAPSHOTTER_CHECKSUM=linux
+# We specifically want the static ones
+TINI_CHECKSUM=static
+
+version::compare(){
+ local raw_version_fd="$1"
+ local parsed
+ local line
+ while read -r line; do
+ parsed+=("$line")
+ done < <(sed -E 's/^(.* )?v?([0-9]+)[.]([0-9]+)([.]([0-9]+))?(-?([a-z]+)[.]?([0-9]+))?.*/\2\n\3\n\5\n\7\n\8\n/i' < "$raw_version_fd")
+
+ local maj="${higher[0]}"
+ local min="${higher[1]}"
+ local patch="${higher[2]}"
+ local sub="${higher[3]}"
+ local subv="${higher[4]}"
+
+ log::debug "parsed version: ${parsed[*]}"
+ log::debug " > current higher version: ${higher[*]}"
+
+ if [ "${parsed[0]}" -gt "$maj" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ elif [ "${parsed[0]}" -lt "$maj" ]; then
+ return 1
+ fi
+ if [ "${parsed[1]}" -gt "$min" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ elif [ "${parsed[1]}" -lt "$min" ]; then
+ return 1
+ fi
+ if [ "${parsed[2]}" -gt "$patch" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ elif [ "${parsed[2]}" -lt "$patch" ]; then
+ return 1
+ fi
+ # If the current latest does not have a sub, then it is more recent
+ if [ "$sub" == "" ]; then
+ return 1
+ fi
+ # If it has a sub, and the parsed one does not, then the parsed one is more recent
+ if [ "${parsed[3]}" == "" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ fi
+ # Otherwise, we have two subs. Normalize, then compare
+ # alpha < beta < rc
+ [ "$sub" == "rc" ] && sub=2 || { [ "$sub" == "beta" ] && sub=1; } || { [ "$sub" == "alpha" ] && sub=0; } || {
+ log::error "Unrecognized sub pattern: $sub"
+ exit 42
+ }
+ [ "${parsed[3]}" == "rc" ] && parsed[3]=2 || { [ "${parsed[3]}" == "beta" ] && parsed[3]=1; } || { [ "${parsed[3]}" == "alpha" ] && parsed[3]=0; } || {
+ log::error "Unrecognized sub pattern: ${parsed[3]}"
+ exit 42
+ }
+ if [ "${parsed[3]}" -gt "$sub" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ elif [ "${parsed[3]}" -lt "$sub" ]; then
+ return 1
+ fi
+ # Ok... we are left with just the sub version
+ if [ "${parsed[4]}" -gt "$subv" ]; then
+ log::debug " > new higher"
+ higher=("${parsed[@]}")
+ return
+ elif [ "${parsed[4]}" -lt "$subv" ]; then
+ return 1
+ fi
+}
+
+# Retrieves the "highest version" release for a given repo
+# Optional argument 2 allows to filter out unwanted release which name matches the argument
+# This is useful for repo that do independent releases for assets (like buildkit dockerfiles)
+latest::release(){
+ local repo="$1"
+ local ignore="${2:-}"
+ local line
+ local name
+
+ higher=(0 0 0 "alpha" 0)
+ higher_data=
+ higher_readable=
+
+ log::info "Analyzing releases for $repo"
+
+ while read -r line; do
+ [ ! "$ignore" ] || ! grep -q "$ignore" <<<"$line" || continue
+ name="$(echo "$line" | jq -rc .name)"
+ if [ "$name" == "" ] || [ "$name" == null ] ; then
+ log::debug " > bogus release name ($name) ignored"
+ continue
+ fi
+ log::debug " > found release: $name"
+ if version::compare <(echo "$line" | jq -rc .name); then
+ higher_data="$line"
+ higher_readable="$(echo "$line" | jq -rc .name | sed -E 's/(.*[ ])?(v?[0-9][0-9.a-z-]+).*/\2/')"
+ fi
+ done < <(github::releases "$repo")
+
+ log::info " >>> latest release detected: $higher_readable"
+}
+
+# Retrieve the latest git tag for a given repo
+latest::tag(){
+ local repo="$1"
+
+ log::info "Analyzing tags for $repo"
+ github::tags::latest "$repo"
+}
+
+# Once a latest release has been retrieved for a given project, you can get the url to the asset matching OS and ARCH
+assets::get(){
+ local os="$1"
+ local arch="$2"
+ local name=
+ local found=
+
+ while read -r line; do
+ name="$(echo "$line" | jq -rc .name)"
+ log::debug " >>> candidate $name"
+ ! grep -qi "$os" <<<"$name" || ! grep -qi "$arch" <<<"$name" || (
+ ! grep -Eqi "[.]t?g?x?z$" <<<"$name" && grep -Eqi "[.][a-z]+$" <<<"$name"
+ ) || {
+ found="$line"
+ break
+ }
+ done < <(echo "$higher_data" | jq -rc .assets.[])
+ [ "$found" == "" ] && {
+ log::warning " >>> no asset found for $os/$arch"
+ } || {
+ log::info " >>> found asset for $os/$arch: $(echo "$found" | jq -rc .browser_download_url)"
+ printf "%s\n" "$(echo "$found" | jq -rc .browser_download_url)"
+ }
+}
+
+######################
+# Script
+######################
+
+canary::build::integration(){
+ docker_args=(docker build -t test-integration --target test-integration)
+
+ for dep in "${dependencies[@]}"; do
+ local bl=""
+ shortname="${dep##*/}"
+ [ "$shortname" != "plugins" ] || shortname="cni-plugins"
+ [ "$shortname" != "fuse-overlayfs-snapshotter" ] || shortname="containerd-fuse-overlayfs"
+ for bl in "${blacklist[@]}"; do
+ if [ "$bl" == "$shortname" ]; then
+ log::warning "Dependency $shortname is blacklisted and will be left to its currently pinned version"
+ break
+ fi
+ done
+ [ "$bl" != "$shortname" ] || continue
+
+ shortsafename="$(printf "%s" "$shortname" | tr '[:lower:]' '[:upper:]' | tr '-' '_')"
+
+ exclusion="${shortsafename}_EXCLUDE"
+ latest::release "$dep" "${!exclusion:-}"
+
+ # XXX containerd does not display "v" in its released versions
+ [ "${higher_readable:0:1}" == v ] || higher_readable="v$higher_readable"
+
+ checksum="${shortsafename}_CHECKSUM"
+ if [ "${!checksum:-}" != "" ]; then
+ # Checksum file
+ checksum_file=./Dockerfile.d/SHA256SUMS.d/"${shortname}-${higher_readable}"
+ if [ ! -e "$checksum_file" ]; then
+ # Get assets - try first os/arch - fallback on gnu style arch otherwise
+ assets=()
+
+ # Most well behaved go projects will tag with a go os and arch
+ candidate="$(assets::get "${!checksum:-}" "amd64")"
+ # Then non go projects tend to use gnu style
+ [ "$candidate" != "" ] || candidate="$(assets::get "" "x86_64")"
+ # And then some projects which are linux only do not specify the OS
+ [ "$candidate" != "" ] || candidate="$(assets::get "" "amd64")"
+ [ "$candidate" == "" ] || assets+=("$candidate")
+
+ candidate="$(assets::get "${!checksum:-}" "arm64")"
+ [ "$candidate" != "" ] || candidate="$(assets::get "" "aarch64")"
+ [ "$candidate" != "" ] || candidate="$(assets::get "" "arm64")"
+ [ "$candidate" == "" ] || assets+=("$candidate")
+ # Fallback to source if there is nothing else
+
+ [ "${#assets[@]}" != 0 ] || candidate="$(assets::get "" "source")"
+ [ "$candidate" == "" ] || assets+=("$candidate")
+
+ # XXX very special...
+ if [ "$shortsafename" == "STARGZ_SNAPSHOTTER" ]; then
+ assets+=("https://raw.githubusercontent.com/containerd/stargz-snapshotter/${higher_readable}/script/config/etc/systemd/system/stargz-snapshotter.service")
+ fi
+
+ # Write the checksum for what we found
+ if [ "${#assets[@]}" == 0 ]; then
+ log::error "No asset found for this checksum-able dependency. Dropping off."
+ exit 1
+ fi
+ http::checksum "${assets[@]}" > "$checksum_file"
+ fi
+ fi
+
+ while read -r line; do
+ # Extract value after "=" from a possible dockerfile `ARG XXX_VERSION`
+ old_version=$(echo "$line" | grep "ARG ${shortsafename}_VERSION=") || true
+ old_version="${old_version##*=}"
+ [ "$old_version" != "" ] || continue
+ # If the Dockerfile version does NOT start with a v, adapt to that
+ [ "${old_version:0:1}" == "v" ] || higher_readable="${higher_readable:1}"
+
+ if [ "$old_version" != "$higher_readable" ]; then
+ log::warning "Dependency ${shortsafename} is going to use an updated version $higher_readable (currently: $old_version)"
+ fi
+ done < ./Dockerfile
+
+ docker_args+=(--build-arg "${shortsafename}_VERSION=$higher_readable")
+ done
+
+ hub_available_go_version="$(canary::golang::hublatest)"
+ if [ "$hub_available_go_version" != "" ]; then
+ docker_args+=(--build-arg "GO_VERSION=$hub_available_go_version")
+ fi
+
+ log::debug "${docker_args[*]} ."
+ "${docker_args[@]}" "."
+}
+
+# Hub usually has a delay before available golang version show-up. This method will find the latest available one.
+# See
+# - https://github.com/containerd/nerdctl/issues/3224
+# - https://github.com/containerd/nerdctl/issues/3306
+canary::golang::hublatest(){
+ local hub_tags
+ local go_version
+ local available_version=""
+ local index
+
+ hub_tags="$(http::get /dev/stdout "https://registry-1.docker.io/v2/library/golang/tags/list" -H "Authorization: Bearer $(http::get /dev/stdout "https://auth.docker.io/token?service=registry.docker.io&scope=repository%3Alibrary%2Fgolang%3Apull" | jq -rc .access_token)")"
+
+ index=0
+ while [ "$available_version" == "" ] && [ "$index" -lt 5 ]; do
+ go_version="$(http::get /dev/stdout "https://go.dev/dl/?mode=json&include=all" | jq -rc .[$index].version)"
+ go_version="${go_version##*go}"
+ available_version="$(printf "%s" "$hub_tags" | jq -rc ".tags[] | select(.==\"$go_version\")")"
+ ((index++))
+ done || true
+
+ printf "%s" "$available_version"
+}
+
+canary::golang::latest(){
+ # Enable extended globbing features to use advanced pattern matching
+ shopt -s extglob
+
+ # Get latest golang version and split it in components
+ norm=()
+ while read -r line; do
+ line_trimmed="${line//+([[:space:]])/}"
+ norm+=("$line_trimmed")
+ done < \
+ <(sed -E 's/^go([0-9]+)[.]([0-9]+)([.]([0-9]+))?(([a-z]+)([0-9]+))?/\1.\2\n\4\n\6\n\7/i' \
+ <(curl -fsSL "https://go.dev/dl/?mode=json&include=all" | jq -rc .[0].version) \
+ )
+
+ # Serialize version, making sure we have a patch version, and separate possible rcX into .rc-X
+ [ "${norm[1]}" != "" ] || norm[1]="0"
+ norm[1]=".${norm[1]}"
+ [ "${norm[2]}" == "" ] || norm[2]="-${norm[2]}"
+ [ "${norm[3]}" == "" ] || norm[3]=".${norm[3]}"
+ # Save it
+ IFS=
+ echo "GO_VERSION=${norm[*]}" >> "$GITHUB_ENV"
+}
diff --git a/hack/build-integration-kubernetes.sh b/hack/build-integration-kubernetes.sh
new file mode 100755
index 00000000000..5647e4dd4f8
--- /dev/null
+++ b/hack/build-integration-kubernetes.sh
@@ -0,0 +1,118 @@
+#!/usr/bin/env bash
+
+# Copyright The containerd Authors.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
+readonly root
+# shellcheck source=/dev/null
+. "$root/scripts/lib.sh"
+
+GO_VERSION=1.23
+KIND_VERSION=v0.24.0
+CNI_PLUGINS_VERSION=v1.5.1
+
+[ "$(uname -m)" == "aarch64" ] && GOARCH=arm64 || GOARCH=amd64
+
+_rootful=
+
+configure::rootful(){
+ log::debug "Configuring rootful to: ${1:+true}"
+ _rootful="${1:+true}"
+}
+
+install::kind(){
+ local version="$1"
+ local temp
+ temp="$(fs::mktemp "install")"
+
+ http::get "$temp"/kind "https://kind.sigs.k8s.io/dl/$version/kind-linux-${GOARCH:-amd64}"
+ host::install "$temp"/kind
+}
+
+# shellcheck disable=SC2120
+install::kubectl(){
+ local version="${1:-}"
+ [ "$version" ] || version="$(http::get /dev/stdout https://dl.k8s.io/release/stable.txt)"
+ local temp
+ temp="$(fs::mktemp "install")"
+
+ http::get "$temp"/kubectl "https://storage.googleapis.com/kubernetes-release/release/$version/bin/linux/${GOARCH:-amd64}/kubectl"
+ host::install "$temp"/kubectl
+}
+
+install::cni(){
+ local version="$1"
+ local temp
+ temp="$(fs::mktemp "install")"
+
+ http::get "$temp"/cni.tgz "https://github.com/containernetworking/plugins/releases/download/$version/cni-plugins-${GOOS:-linux}-${GOARCH:-amd64}-$version.tgz"
+ sudo mkdir -p /opt/cni/bin
+ sudo tar xzf "$temp"/cni.tgz -C /opt/cni/bin
+ mkdir -p ~/opt/cni/bin
+ tar xzf "$temp"/cni.tgz -C ~/opt/cni/bin
+ rm "$temp"/cni.tgz
+}
+
+exec::kind(){
+ local args=()
+ [ ! "$_rootful" ] || args=(sudo env PATH="$PATH" KIND_EXPERIMENTAL_PROVIDER="$KIND_EXPERIMENTAL_PROVIDER")
+ args+=(kind)
+
+ log::debug "${args[*]} $*"
+ "${args[@]}" "$@"
+}
+
+exec::nerdctl(){
+ local args=()
+ [ ! "$_rootful" ] || args=(sudo env PATH="$PATH")
+ args+=("$(pwd)"/_output/nerdctl)
+
+ log::debug "${args[*]} $*"
+ "${args[@]}" "$@"
+}
+
+# Install dependencies
+main(){
+ log::info "Configuring rootful"
+ configure::rootful "${ROOTFUL:-}"
+
+ log::info "Installing host dependencies if necessary"
+ host::require kind 2>/dev/null || install::kind "$KIND_VERSION"
+ host::require kubectl 2>/dev/null || install::kubectl
+
+ # Build nerdctl to use for kind
+ make binaries
+ PATH=$(pwd)/_output:"$PATH"
+ export PATH
+
+ # Add CNI plugins
+ install::cni "$CNI_PLUGINS_VERSION"
+
+ # Hack to get go into kind control plane
+ exec::nerdctl rm -f go-kind 2>/dev/null || true
+ exec::nerdctl run -d --quiet --name go-kind golang:"$GO_VERSION" sleep Inf
+ exec::nerdctl cp go-kind:/usr/local/go /tmp/go
+ exec::nerdctl rm -f go-kind
+
+ # Create fresh cluster
+ log::info "Creating new cluster"
+ export KIND_EXPERIMENTAL_PROVIDER=nerdctl
+ exec::kind delete cluster --name nerdctl-test 2>/dev/null || true
+ exec::kind create cluster --name nerdctl-test --config=./hack/kind.yaml
+}
+
+main "$@"
diff --git a/hack/configure-windows-ci.ps1 b/hack/configure-windows-ci.ps1
index 0d6476e0839..1c97a487182 100644
--- a/hack/configure-windows-ci.ps1
+++ b/hack/configure-windows-ci.ps1
@@ -1,7 +1,6 @@
-$ErrorActionPreference = "Stop"
+# To install CNI, see https://github.com/containerd/containerd/blob/release/1.7/script/setup/install-cni-windows
-#install build dependencies
-choco install --limitoutput --no-progress -y git golang
+$ErrorActionPreference = "Stop"
#install containerd
$version=$env:ctrdVersion
@@ -9,46 +8,12 @@ echo "Installing containerd $version"
curl.exe -L https://github.com/containerd/containerd/releases/download/v$version/containerd-$version-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz
tar.exe xvf containerd-windows-amd64.tar.gz
mkdir -force "$Env:ProgramFiles\containerd"
-mv ./bin/* "$Env:ProgramFiles\containerd"
+cp ./bin/* "$Env:ProgramFiles\containerd"
& $Env:ProgramFiles\containerd\containerd.exe config default | Out-File "$Env:ProgramFiles\containerd\config.toml" -Encoding ascii
& $Env:ProgramFiles\containerd\containerd.exe --register-service
Start-Service containerd
-#configure cni
-mkdir -force "$Env:ProgramFiles\containerd\cni\bin"
-mkdir -force "$Env:ProgramFiles\containerd\cni\conf"
-curl.exe -LO https://github.com/microsoft/windows-container-networking/releases/download/v0.2.0/windows-container-networking-cni-amd64-v0.2.0.zip
-Expand-Archive windows-container-networking-cni-amd64-v0.2.0.zip -DestinationPath "$Env:ProgramFiles\containerd\cni\bin" -Force
-
-curl.exe -LO https://raw.githubusercontent.com/microsoft/SDN/master/Kubernetes/windows/hns.psm1
-ipmo ./hns.psm1
-
-# cirrus already has nat net work configured for docker. We can re-use that for testing
-$sn=(get-hnsnetwork | ? Name -Like "nat" | select -ExpandProperty subnets)
-$subnet=$sn.AddressPrefix
-$gateway=$sn.GatewayAddress
-@"
-{
- "cniVersion": "0.2.0",
- "name": "nat",
- "type": "nat",
- "master": "Ethernet",
- "ipam": {
- "subnet": "$subnet",
- "routes": [
- {
- "gateway": "$gateway"
- }
- ]
- },
- "capabilities": {
- "portMappings": true,
- "dns": true
- }
-}
-"@ | Set-Content "$Env:ProgramFiles\containerd\cni\conf\0-containerd-nat.conf" -Force
-
echo "configuration complete! Printing configuration..."
echo "Service:"
get-service containerd
diff --git a/hack/generate-release-note.sh b/hack/generate-release-note.sh
index 877853566e5..70276919be3 100755
--- a/hack/generate-release-note.sh
+++ b/hack/generate-release-note.sh
@@ -17,15 +17,15 @@
minimal_amd64tgz="$(find _output -name '*linux-amd64.tar.gz*' -and ! -name '*full*')"
full_amd64tgz="$(find _output -name '*linux-amd64.tar.gz*' -and -name '*full*')"
-minimal_amd64tgz_basename="$(basename ${minimal_amd64tgz})"
-full_amd64tgz_basename="$(basename ${full_amd64tgz})"
+minimal_amd64tgz_basename="$(basename "${minimal_amd64tgz}")"
+full_amd64tgz_basename="$(basename "${full_amd64tgz}")"
cat <<-EOX
## Changes
(To be documented)
## Compatible containerd versions
-This release of nerdctl is expected to be used with containerd v1.6 or v1.7.
+This release of nerdctl is expected to be used with containerd v1.6, v1.7, or v2.0.
## About the binaries
- Minimal (\`${minimal_amd64tgz_basename}\`): nerdctl only
@@ -37,7 +37,7 @@ Extract the archive to a path like \`/usr/local/bin\` or \`~/bin\` .
\`\`\`
-$(tar tzvf ${minimal_amd64tgz})
+$(tar tzvf "${minimal_amd64tgz}")
\`\`\`
@@ -49,7 +49,7 @@ Extract the archive to a path like \`/usr/local\` or \`~/.local\` .
\`\`\`
-$(tar tzvf ${full_amd64tgz})
+$(tar tzvf "${full_amd64tgz}")
\`\`\`
@@ -59,7 +59,7 @@ $(tar tzvf ${full_amd64tgz})
See \`share/doc/nerdctl-full/README.md\`:
\`\`\`markdown
-$(tar xOzf ${full_amd64tgz} share/doc/nerdctl-full/README.md)
+$(tar xOzf "${full_amd64tgz}" share/doc/nerdctl-full/README.md)
\`\`\`
diff --git a/hack/kind.yaml b/hack/kind.yaml
new file mode 100644
index 00000000000..c6439c02458
--- /dev/null
+++ b/hack/kind.yaml
@@ -0,0 +1,14 @@
+# https://pkg.go.dev/sigs.k8s.io/kind/pkg/apis/config/v1alpha4#Cluster
+kind: Cluster
+apiVersion: kind.x-k8s.io/v1alpha4
+nodes:
+ - role: control-plane
+ extraMounts:
+ - hostPath: _output/nerdctl
+ containerPath: /usr/local/bin/nerdctl
+ - hostPath: /tmp/go
+ containerPath: /usr/local/go
+ - hostPath: .
+ containerPath: /nerdctl-source
+ - hostPath: /opt/cni
+ containerPath: /opt/cni
diff --git a/hack/lint-imports.sh b/hack/lint-imports.sh
new file mode 100755
index 00000000000..d37df1a09bf
--- /dev/null
+++ b/hack/lint-imports.sh
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+# Copyright The containerd Authors.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# FIXME: goimports-reviser is currently broken when it comes to ./...
+# Specifically, it will ignore arguments, and will return exit 0 regardless
+# This here is a workaround, until they fix it upstream: https://github.com/incu6us/goimports-reviser/pull/157
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+
+ex=0
+
+while read -r file; do
+ goimports-reviser -list-diff -set-exit-status -output stdout -company-prefixes "github.com/containerd" "$file" || {
+ ex=$?
+ >&2 printf "Imports are not listed properly in %s. Consider calling make lint-fix-imports.\n" "$file"
+ }
+done < <(find ./ -type f -name '*.go')
+
+exit "$ex"
diff --git a/hack/scripts/lib.sh b/hack/scripts/lib.sh
new file mode 100755
index 00000000000..c29b056ea70
--- /dev/null
+++ b/hack/scripts/lib.sh
@@ -0,0 +1,237 @@
+#!/usr/bin/env bash
+
+# Copyright The containerd Authors.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+
+## This is a library of generic helpers that can be used across different projects
+
+# Simple logger
+readonly LOG_LEVEL_DEBUG=0
+readonly LOG_LEVEL_INFO=1
+readonly LOG_LEVEL_WARNING=2
+readonly LOG_LEVEL_ERROR=3
+
+readonly LOG_COLOR_BLACK=0
+readonly LOG_COLOR_RED=1
+readonly LOG_COLOR_GREEN=2
+readonly LOG_COLOR_YELLOW=3
+readonly LOG_COLOR_BLUE=4
+readonly LOG_COLOR_MAGENTA=5
+readonly LOG_COLOR_CYAN=6
+readonly LOG_COLOR_WHITE=7
+readonly LOG_COLOR_DEFAULT=9
+
+readonly LOG_STYLE_DEBUG=( setaf "$LOG_COLOR_WHITE" )
+readonly LOG_STYLE_INFO=( setaf "$LOG_COLOR_GREEN" )
+readonly LOG_STYLE_WARNING=( setaf "$LOG_COLOR_YELLOW" )
+readonly LOG_STYLE_ERROR=( setaf "$LOG_COLOR_RED" )
+
+_log::log(){
+ local level
+ local style
+ local numeric_level
+ local message="$2"
+
+ level="$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]')"
+ numeric_level="$(printf "LOG_LEVEL_%s" "$level")"
+ style="LOG_STYLE_${level}[@]"
+
+ [ "${!numeric_level}" -ge "$LOG_LEVEL" ] || return 0
+
+ [ ! "$TERM" ] || [ ! -t 2 ] || >&2 tput "${!style}" 2>/dev/null || true
+ >&2 printf "[%s] %s: %s\n" "$(date 2>/dev/null || true)" "$(printf "%s" "$level" | tr '[:lower:]' '[:upper:]')" "$message"
+ [ ! "$TERM" ] || [ ! -t 2 ] || >&2 tput op 2>/dev/null || true
+}
+
+log::init(){
+ local _ll
+ # Default log to warning if unspecified
+ _ll="$(printf "LOG_LEVEL_%s" "${LOG_LEVEL:-warning}" | tr '[:lower:]' '[:upper:]')"
+ # Default to 3 (warning) if unrecognized
+ LOG_LEVEL="${!_ll:-3}"
+}
+
+log::debug(){
+ _log::log debug "$@"
+}
+
+log::info(){
+ _log::log info "$@"
+}
+
+log::warning(){
+ _log::log warning "$@"
+}
+
+log::error(){
+ _log::log error "$@"
+}
+
+# Helpers
+host::require(){
+ local binary="$1"
+
+ log::debug "Checking presence of $binary"
+ command -v "$binary" >/dev/null || {
+ log::error "You need $binary for this script to work, and it cannot be found in your path"
+ return 1
+ }
+}
+
+host::install(){
+ local binary
+
+ for binary in "$@"; do
+ log::debug "sudo install -D -m 755 $binary /usr/local/bin/$(basename "$binary")"
+ sudo install -D -m 755 "$binary" /usr/local/bin/"$(basename "$binary")"
+ done
+}
+
+fs::mktemp(){
+ local prefix="${1:-temporary}"
+
+ mktemp -dq "${TMPDIR:-/tmp}/$prefix.XXXXXX" 2>/dev/null || mktemp -dq || {
+ log::error "Failed to create temporary directory"
+ return 1
+ }
+}
+
+tar::expand(){
+ local dir="$1"
+ local arc="$2"
+
+ log::debug "tar -C $dir -xzf $arc"
+ tar -C "$dir" -xzf "$arc"
+}
+
+_http::get(){
+ local url="$1"
+ local output="$2"
+ local retry="$3"
+ local delay="$4"
+ local user="${5:-}"
+ local password="${6:-}"
+ shift
+ shift
+ shift
+ shift
+ shift
+ shift
+
+ local header
+ local command=(curl -fsSL --retry "$retry" --retry-delay "$delay" -o "$output")
+ # Add a basic auth user if necessary
+ [ "$user" == "" ] || command+=(--user "$user:$password")
+ # Force tls v1.2 and no redirect to http if url scheme is https
+ [ "${url:0:5}" != "https" ] || command+=(--proto '=https' --tlsv1.2)
+ # Stuff in any additional arguments as headers
+ for header in "$@"; do
+ command+=(-H "$header")
+ done
+ # Debug
+ log::debug "${command[*]} $url"
+ # Exec
+ "${command[@]}" "$url" || {
+ log::error "Failed to connect to $url with $retry retries every $delay seconds"
+ return 1
+ }
+}
+
+http::get(){
+ local output="$1"
+ local url="$2"
+ shift
+ shift
+
+ _http::get "$url" "$output" "2" "1" "" "" "$@"
+}
+
+http::healthcheck(){
+ local url="$1"
+ local retry="${2:-5}"
+ local delay="${3:-1}"
+ local user="${4:-}"
+ local password="${5:-}"
+ shift
+ shift
+ shift
+ shift
+ shift
+
+ _http::get "$url" /dev/null "$retry" "$delay" "$user" "$password" "$@"
+}
+
+http::checksum(){
+ local urls=("$@")
+ local url
+
+ local temp
+ temp="$(fs::mktemp "http-checksum")"
+
+ host::require shasum
+
+ for url in "${urls[@]}"; do
+ http::get "$temp/${url##*/}" "$url"
+ done
+
+ cd "$temp"
+ shasum -a 256 ./*
+ cd - >/dev/null || true
+}
+
+# Github API helpers
+# Set GITHUB_TOKEN to use authenticated requests to workaround limitations
+
+github::settoken(){
+ local token="$1"
+ # If passed token is a github action pattern replace, and we are NOT on github, ignore it
+ # shellcheck disable=SC2016
+ [ "${token:0:3}" == '${{' ] || GITHUB_TOKEN="$token"
+}
+
+github::request(){
+ local endpoint="$1"
+ local args=(
+ "Accept: application/vnd.github+json"
+ "X-GitHub-Api-Version: 2022-11-28"
+ )
+
+ [ "${GITHUB_TOKEN:-}" == "" ] || args+=("Authorization: Bearer $GITHUB_TOKEN")
+
+ http::get /dev/stdout https://api.github.com/"$endpoint" "${args[@]}"
+}
+
+github::tags::latest(){
+ local repo="$1"
+ github::request "repos/$repo/tags" | jq -rc .[0].name
+}
+
+github::releases(){
+ local repo="$1"
+ github::request "repos/$repo/releases" |
+ jq -rc .[]
+}
+
+github::releases::latest(){
+ local repo="$1"
+ github::request "repos/$repo/releases/latest" | jq -rc .
+}
+
+log::init
+host::require jq
+host::require tar
+host::require curl
diff --git a/hack/test-integration.sh b/hack/test-integration.sh
new file mode 100755
index 00000000000..73e2b4ebb19
--- /dev/null
+++ b/hack/test-integration.sh
@@ -0,0 +1,49 @@
+#!/usr/bin/env bash
+
+# Copyright The containerd Authors.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+# http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# shellcheck disable=SC2034,SC2015
+set -o errexit -o errtrace -o functrace -o nounset -o pipefail
+root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)"
+readonly root
+
+readonly timeout="60m"
+readonly retries="2"
+readonly needsudo="${WITH_SUDO:-}"
+
+# See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization
+args=(--format=testname --jsonfile /tmp/test-integration.log --packages="$root"/../cmd/nerdctl/...)
+
+if [ "$#" == 0 ]; then
+ "$root"/test-integration.sh -test.only-flaky=false
+ "$root"/test-integration.sh -test.only-flaky=true
+ exit
+fi
+
+for arg in "$@"; do
+ if [ "$arg" == "-test.only-flaky=true" ] || [ "$arg" == "-test.only-flaky" ]; then
+ args+=("--rerun-fails=$retries")
+ break
+ fi
+done
+
+if [ "$needsudo" == "true" ] || [ "$needsudo" == "yes" ] || [ "$needsudo" == "1" ]; then
+ gotestsum "${args[@]}" -- -timeout="$timeout" -p 1 -exec sudo -args -test.allow-kill-daemon "$@"
+else
+ gotestsum "${args[@]}" -- -timeout="$timeout" -p 1 -args -test.allow-kill-daemon "$@"
+fi
+
+echo "These are the tests that took more than 10 seconds:"
+gotestsum tool slowest --threshold 10s --jsonfile /tmp/test-integration.log
diff --git a/hack/verify-pkg-isolation.sh b/hack/verify-pkg-isolation.sh
index e6b2150608a..3f1edd22e30 100755
--- a/hack/verify-pkg-isolation.sh
+++ b/hack/verify-pkg-isolation.sh
@@ -17,7 +17,7 @@
echo "Verifying that ./pkg/... is decoupled from the CLI packages"
set -eux -o pipefail
-if go list -f '{{join .Deps "\n"}}' ./pkg/... | grep -E '^(github.com/spf13/cobra|github.com/spf13/pflag|github.com/containerd/nerdctl/cmd)'; then
+if go list -f '{{join .Deps "\n"}}' ./pkg/... | grep -E '^(github.com/spf13/cobra|github.com/spf13/pflag|github.com/containerd/nerdctl/v2/cmd)'; then
echo >&2 "ERROR: ./pkg/... is not decoupled from the CLI packages"
exit 1
fi
diff --git a/pkg/annotations/annotations.go b/pkg/annotations/annotations.go
new file mode 100644
index 00000000000..97c1edd56ef
--- /dev/null
+++ b/pkg/annotations/annotations.go
@@ -0,0 +1,44 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package annotations defines OCI annotations
+package annotations
+
+const (
+ // Prefix is the common prefix of nerdctl annotations
+ Prefix = "nerdctl/"
+
+ // Bypass4netns is the flag for acceleration with bypass4netns
+ // Boolean value which can be parsed with strconv.ParseBool() is required.
+ // (like "nerdctl/bypass4netns=true" or "nerdctl/bypass4netns=false")
+ Bypass4netns = Prefix + "bypass4netns"
+
+ // Bypass4netnsIgnoreSubnets is a JSON of []string that is appended to
+ // the `bypass4netns --ignore` list.
+ Bypass4netnsIgnoreSubnets = Bypass4netns + "-ignore-subnets"
+
+ // Bypass4netnsIgnoreBind disables acceleration for bind.
+ // Boolean value which can be parsed with strconv.ParseBool() is required.
+ Bypass4netnsIgnoreBind = Bypass4netns + "-ignore-bind"
+)
+
+var ShellCompletions = []string{
+ Bypass4netns + "=true",
+ Bypass4netns + "=false",
+ Bypass4netnsIgnoreSubnets + "=",
+ Bypass4netnsIgnoreBind + "=true",
+ Bypass4netnsIgnoreBind + "=false",
+}
diff --git a/pkg/api/types/builder_types.go b/pkg/api/types/builder_types.go
index 76e8934089e..b9574aebcc6 100644
--- a/pkg/api/types/builder_types.go
+++ b/pkg/api/types/builder_types.go
@@ -43,6 +43,10 @@ type BuilderBuildOptions struct {
Progress string
// Secret file to expose to the build: id=mysecret,src=/local/secret
Secret []string
+ // Allow extra privileged entitlement, e.g. network.host, security.insecure
+ Allow []string
+ // Attestation parameters (format: "type=sbom,generator=image")"
+ Attest []string
// SSH agent socket or keys to expose to the build (format: default|[=|[,]])
SSH []string
// Quiet suppress the build output and print image ID on success
@@ -61,6 +65,14 @@ type BuilderBuildOptions struct {
Label []string
// BuildContext is the build context
BuildContext string
+ // ExtendedBuildContext is a pair of key=value (e.g. myorg/myapp=docker-image://path/to/image, dir2=/path/to/dir2)
+ ExtendedBuildContext []string
+ // NetworkMode mode for the build context
+ NetworkMode string
+ // Pull determines if we should try to pull latest image from remote. Default is buildkit's default.
+ Pull *bool
+ // ExtraHosts is a set of custom host-to-IP mappings.
+ ExtraHosts []string
}
// BuilderPruneOptions specifies options for `nerdctl builder prune`.
@@ -72,4 +84,6 @@ type BuilderPruneOptions struct {
BuildKitHost string
// All will remove all unused images and all build cache, not just dangling ones
All bool
+ // Force will not prompt for confirmation.
+ Force bool
}
diff --git a/pkg/api/types/container_network_types.go b/pkg/api/types/container_network_types.go
index c4476662b9d..c4b5971240c 100644
--- a/pkg/api/types/container_network_types.go
+++ b/pkg/api/types/container_network_types.go
@@ -17,7 +17,7 @@
package types
import (
- gocni "github.com/containerd/go-cni"
+ "github.com/containerd/go-cni"
)
// NetworkOptions struct defining networking-related options.
@@ -28,6 +28,8 @@ type NetworkOptions struct {
MACAddress string
// IPAddress set specific static IP address(es) to use
IPAddress string
+ // IP6Address set specific static IP6 address(es) to use
+ IP6Address string
// Hostname set container host name
Hostname string
// DNSServers set custom DNS servers
@@ -41,5 +43,5 @@ type NetworkOptions struct {
// UTS namespace to use
UTSNamespace string
// PortMappings specifies a list of ports to publish from the container to the host
- PortMappings []gocni.PortMapping
+ PortMappings []cni.PortMapping
}
diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go
index a107f16cac1..13e798cecad 100644
--- a/pkg/api/types/container_types.go
+++ b/pkg/api/types/container_types.go
@@ -62,10 +62,14 @@ type ContainerCreateOptions struct {
Interactive bool
// TTY specifies whether to allocate a pseudo-TTY for the container
TTY bool
+ // SigProxy specifies whether to proxy all received signals to the process
+ SigProxy bool
// Detach runs container in background and print container ID
Detach bool
// The key sequence for detaching a container.
DetachKeys string
+ // Attach STDIN, STDOUT, or STDERR
+ Attach []string
// Restart specifies the policy to apply when a container exits
Restart string
// Rm specifies whether to remove the container automatically when it exits
@@ -169,6 +173,8 @@ type ContainerCreateOptions struct {
CapDrop []string
// Privileged gives extended privileges to this container
Privileged bool
+ // Systemd
+ Systemd string
// #endregion
// #region for runtime flags
@@ -185,6 +191,8 @@ type ContainerCreateOptions struct {
Tmpfs []string
// Mount specifies a list of mounts to mount
Mount []string
+ // VolumesFrom specifies a list of specified containers to mount from
+ VolumesFrom []string
// #endregion
// #region for rootfs flags
@@ -213,9 +221,12 @@ type ContainerCreateOptions struct {
// Name assign a name to the container
Name string
// Label set meta data on a container
+ // (not passed through to the OCI runtime since nerdctl v2.0, with an exception for "nerdctl/bypass4netns")
Label []string
// LabelFile read in a line delimited file of labels
LabelFile []string
+ // Annotations set meta data on a container (passed through to the OCI runtime)
+ Annotations []string
// CidFile write the container ID to the file
CidFile string
// PidFile specifies the file path to write the task's pid. The CLI syntax conforms to Podman convention.
@@ -323,6 +334,8 @@ type ContainerInspectOptions struct {
GOptions GlobalCommandOptions
// Format of the output
Format string
+ // Whether to report the size
+ Size bool
// Inspect mode, either dockercompat or native
Mode string
}
@@ -342,6 +355,13 @@ type ContainerCommitOptions struct {
Pause bool
}
+// ContainerDiffOptions specifies options for `nerdctl (container) diff`.
+type ContainerDiffOptions struct {
+ Stdout io.Writer
+ // GOptions is the global options
+ GOptions GlobalCommandOptions
+}
+
// ContainerLogsOptions specifies options for `nerdctl (container) logs`.
type ContainerLogsOptions struct {
Stdout io.Writer
@@ -368,6 +388,18 @@ type ContainerWaitOptions struct {
GOptions GlobalCommandOptions
}
+// ContainerAttachOptions specifies options for `nerdctl (container) attach`.
+type ContainerAttachOptions struct {
+ Stdin io.Reader
+ Stdout io.Writer
+ Stderr io.Writer
+
+ // GOptions is the global options.
+ GOptions GlobalCommandOptions
+ // DetachKeys is the key sequences to detach from the container.
+ DetachKeys string
+}
+
// ContainerExecOptions specifies options for `nerdctl (container) exec`
type ContainerExecOptions struct {
GOptions GlobalCommandOptions
@@ -391,7 +423,6 @@ type ContainerExecOptions struct {
// ContainerListOptions specifies options for `nerdctl (container) list`.
type ContainerListOptions struct {
- Stdout io.Writer
// GOptions is the global options.
GOptions GlobalCommandOptions
// Show all containers (default shows just running).
@@ -401,21 +432,19 @@ type ContainerListOptions struct {
LastN int
// Truncate output (e.g., container ID, command of the container main process, etc.) or not.
Truncate bool
- // Only display container IDs.
- Quiet bool
// Display total file sizes.
Size bool
- // Format the output using the given Go template (e.g., '{{json .}}', 'table', 'wide').
- Format string
// Filters matches containers based on given conditions.
Filters []string
}
// ContainerCpOptions specifies options for `nerdctl (container) cp`
type ContainerCpOptions struct {
+ // GOptions is the global options.
+ GOptions GlobalCommandOptions
+ // ContainerReq is name, short ID, or long ID of container to copy to/from.
+ ContainerReq string
Container2Host bool
- // Process id
- Pid int
// Destination path to copy file to.
DestPath string
// Source path to copy file from.
@@ -423,3 +452,19 @@ type ContainerCpOptions struct {
// Follow symbolic links in SRC_PATH
FollowSymLink bool
}
+
+// ContainerStatsOptions specifies options for `nerdctl stats`.
+type ContainerStatsOptions struct {
+ Stdout io.Writer
+ Stderr io.Writer
+ // GOptions is the global options.
+ GOptions GlobalCommandOptions
+ // Show all containers (default shows just running).
+ All bool
+ // Pretty-print images using a Go template, e.g., {{json .}}.
+ Format string
+ // Disable streaming stats and only pull the first result.
+ NoStream bool
+ // Do not truncate output.
+ NoTrunc bool
+}
diff --git a/pkg/api/types/global.go b/pkg/api/types/global.go
index bffb23eee26..132fb17e137 100644
--- a/pkg/api/types/global.go
+++ b/pkg/api/types/global.go
@@ -16,6 +16,6 @@
package types
-import "github.com/containerd/nerdctl/pkg/config"
+import "github.com/containerd/nerdctl/v2/pkg/config"
type GlobalCommandOptions config.Config
diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go
index 51fd3bc4bff..0a723ca2cd2 100644
--- a/pkg/api/types/image_types.go
+++ b/pkg/api/types/image_types.go
@@ -16,7 +16,11 @@
package types
-import "io"
+import (
+ "io"
+
+ v1 "github.com/opencontainers/image-spec/specs-go/v1"
+)
// ImageListOptions specifies options for `nerdctl image list`.
type ImageListOptions struct {
@@ -80,6 +84,13 @@ type ImageConvertOptions struct {
EstargzKeepDiffID bool
// #endregion
+ // #region zstd flags
+ // Zstd convert legacy tar(.gz) layers to zstd. Should be used in conjunction with '--oci'
+ Zstd bool
+ // ZstdCompressionLevel zstd compression level
+ ZstdCompressionLevel int
+ // #endregion
+
// #region zstd:chunked flags
// ZstdChunked convert legacy tar(.gz) layers to zstd:chunked for lazy pulling. Should be used in conjunction with '--oci'
ZstdChunked bool
@@ -152,6 +163,7 @@ type ImagePushOptions struct {
Stdout io.Writer
GOptions GlobalCommandOptions
SignOptions ImageSignOptions
+ SociOptions SociOptions
// Platforms convert content for a specific platform
Platforms []string
// AllPlatforms convert content for all platforms
@@ -169,22 +181,34 @@ type ImagePushOptions struct {
AllowNondistributableArtifacts bool
}
+// RemoteSnapshotterFlags are used for pulling with remote snapshotters
+// e.g. SOCI, stargz, overlaybd
+type RemoteSnapshotterFlags struct {
+ SociIndexDigest string
+}
+
// ImagePullOptions specifies options for `nerdctl (image) pull`.
type ImagePullOptions struct {
- Stdout io.Writer
- Stderr io.Writer
+ Stdout io.Writer
+ Stderr io.Writer
+ // ProgressOutputToStdout directs progress output to stdout instead of stderr
+ ProgressOutputToStdout bool
+
GOptions GlobalCommandOptions
VerifyOptions ImageVerifyOptions
- // Unpack the image for the current single platform (auto/true/false)
- Unpack string
- // Pull content for a specific platform
- Platform []string
- // Pull content for all platforms
- AllPlatforms bool
+ // Unpack the image for the current single platform.
+ // If nil, it will unpack automatically if only 1 platform is specified.
+ Unpack *bool
+ // Content for specific platforms. Empty if `--all-platforms` is true
+ OCISpecPlatform []v1.Platform
+ // Pull mode
+ Mode string
// Suppress verbose output
Quiet bool
// multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)
IPFSAddress string
+ // Flags to pass into remote snapshotters
+ RFlags RemoteSnapshotterFlags
}
// ImageTagOptions specifies options for `nerdctl (image) tag`.
@@ -215,6 +239,8 @@ type ImagePruneOptions struct {
GOptions GlobalCommandOptions
// All Remove all unused images, not just dangling ones.
All bool
+ // Filters output based on conditions provided for the --filter argument
+ Filters []string
// Force will not prompt for confirmation.
Force bool
}
@@ -256,3 +282,11 @@ type ImageVerifyOptions struct {
// CosignCertificateOidcIssuerRegexp A regular expression alternative to --certificate-oidc-issuer for --verify=cosign. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --cosign-certificate-oidc-issuer or --cosign-certificate-oidc-issuer-regexp must be set for keyless flows
CosignCertificateOidcIssuerRegexp string
}
+
+// SociOptions contains options for SOCI.
+type SociOptions struct {
+ // Span size that soci index uses to segment layer data. Default is 4 MiB.
+ SpanSize int64
+ // Minimum layer size to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
+ MinLayerSize int64
+}
diff --git a/pkg/api/types/load_types.go b/pkg/api/types/load_types.go
index df115234cdd..d3ceaf350b1 100644
--- a/pkg/api/types/load_types.go
+++ b/pkg/api/types/load_types.go
@@ -29,4 +29,6 @@ type ImageLoadOptions struct {
Platform []string
// AllPlatforms import content for all platforms
AllPlatforms bool
+ // Quiet suppresses the load output.
+ Quiet bool
}
diff --git a/pkg/api/types/network_types.go b/pkg/api/types/network_types.go
index 4a65ff2122d..5cb26b3ea15 100644
--- a/pkg/api/types/network_types.go
+++ b/pkg/api/types/network_types.go
@@ -18,16 +18,23 @@ package types
import (
"io"
-
- "github.com/containerd/nerdctl/pkg/netutil"
)
// NetworkCreateOptions specifies options for `nerdctl network create`.
type NetworkCreateOptions struct {
// GOptions is the global options
GOptions GlobalCommandOptions
- // CreateOptions is the option for creating network
- CreateOptions netutil.CreateOptions
+
+ Name string
+ Driver string
+ Options map[string]string
+ IPAMDriver string
+ IPAMOptions map[string]string
+ Subnets []string
+ Gateway string
+ IPRange string
+ Labels []string
+ IPv6 bool
}
// NetworkInspectOptions specifies options for `nerdctl network inspect`.
@@ -52,6 +59,8 @@ type NetworkListOptions struct {
Quiet bool
// Format the output using the given Go template, e.g, '{{json .}}', 'wide'
Format string
+ // Filter matches network based on given conditions
+ Filters []string
}
// NetworkPruneOptions specifies options for `nerdctl network prune`.
diff --git a/pkg/api/types/system_types.go b/pkg/api/types/system_types.go
index 65df3218674..bfadba7a057 100644
--- a/pkg/api/types/system_types.go
+++ b/pkg/api/types/system_types.go
@@ -37,6 +37,8 @@ type SystemEventsOptions struct {
GOptions GlobalCommandOptions
// Format the output using the given Go template, e.g, '{{json .}}
Format string
+ // Filter events based on given conditions
+ Filters []string
}
// SystemPruneOptions specifies options for `nerdctl system prune`.
diff --git a/pkg/api/types/volume_types.go b/pkg/api/types/volume_types.go
index 8fd67cecb09..903d8e68698 100644
--- a/pkg/api/types/volume_types.go
+++ b/pkg/api/types/volume_types.go
@@ -54,6 +54,8 @@ type VolumeListOptions struct {
type VolumePruneOptions struct {
Stdout io.Writer
GOptions GlobalCommandOptions
+ //Remove all unused volumes, not just anonymous ones
+ All bool
// Do not prompt for confirmation
Force bool
}
diff --git a/pkg/apparmorutil/apparmorutil_linux.go b/pkg/apparmorutil/apparmorutil_linux.go
index 901940180d0..600047a54da 100644
--- a/pkg/apparmorutil/apparmorutil_linux.go
+++ b/pkg/apparmorutil/apparmorutil_linux.go
@@ -23,9 +23,10 @@ import (
"strings"
"sync"
- "github.com/containerd/containerd/pkg/apparmor"
- "github.com/containerd/containerd/pkg/userns"
- "github.com/sirupsen/logrus"
+ "github.com/moby/sys/userns"
+
+ "github.com/containerd/containerd/v2/pkg/apparmor"
+ "github.com/containerd/log"
)
// CanLoadNewProfile returns whether the current process can load a new AppArmor profile.
@@ -73,7 +74,7 @@ func CanApplySpecificExistingProfile(profileName string) bool {
cmd := exec.Command("aa-exec", "-p", profileName, "--", "true")
out, err := cmd.CombinedOutput()
if err != nil {
- logrus.WithError(err).Debugf("failed to run %v: %q", cmd.Args, string(out))
+ log.L.WithError(err).Debugf("failed to run %v: %q", cmd.Args, string(out))
return false
}
return true
@@ -101,7 +102,7 @@ func Profiles() ([]Profile, error) {
namePath := filepath.Join(profilesPath, ent.Name(), "name")
b, err := os.ReadFile(namePath)
if err != nil {
- logrus.WithError(err).Warnf("failed to read %q", namePath)
+ log.L.WithError(err).Warnf("failed to read %q", namePath)
continue
}
profile := Profile{
@@ -110,7 +111,7 @@ func Profiles() ([]Profile, error) {
modePath := filepath.Join(profilesPath, ent.Name(), "mode")
b, err = os.ReadFile(modePath)
if err != nil {
- logrus.WithError(err).Warnf("failed to read %q", namePath)
+ log.L.WithError(err).Warnf("failed to read %q", namePath)
} else {
profile.Mode = strings.TrimSpace(string(b))
}
diff --git a/pkg/buildkitutil/buildkitutil.go b/pkg/buildkitutil/buildkitutil.go
index 7a5ac2fd02f..5b9570a1ddb 100644
--- a/pkg/buildkitutil/buildkitutil.go
+++ b/pkg/buildkitutil/buildkitutil.go
@@ -34,10 +34,12 @@ import (
"os/exec"
"path/filepath"
"runtime"
+ "slices"
+ "strings"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/hashicorp/go-multierror"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
const (
@@ -57,37 +59,24 @@ func BuildctlBaseArgs(buildkitHost string) []string {
}
func GetBuildkitHost(namespace string) (string, error) {
- if namespace == "" {
- return "", fmt.Errorf("namespace must be specified")
- }
- // Try candidate locations of the current containerd namespace.
- run := "/run/"
- if rootlessutil.IsRootless() {
- var err error
- run, err = rootlessutil.XDGRuntimeDir()
- if err != nil {
- logrus.Warn(err)
- run = fmt.Sprintf("/run/user/%d", rootlessutil.ParentEUID())
- }
- }
- var hostRel []string
- if namespace != "default" {
- hostRel = append(hostRel, fmt.Sprintf("buildkit-%s/buildkitd.sock", namespace))
+ paths, err := getBuildkitHostCandidates(namespace)
+ if err != nil {
+ return "", err
}
- hostRel = append(hostRel, "buildkit-default/buildkitd.sock", "buildkit/buildkitd.sock")
- var allErr error
- for _, p := range hostRel {
- logrus.Debugf("Choosing the buildkit host %q, candidates=%v (in %q)", p, hostRel, run)
- buildkitHost := "unix://" + filepath.Join(run, p)
+
+ var errs []error //nolint:prealloc
+ for _, buildkitHost := range paths {
+ log.L.Debugf("Choosing the buildkit host %q, candidates=%v", buildkitHost, paths)
_, err := pingBKDaemon(buildkitHost)
if err == nil {
- logrus.Debugf("Chosen buildkit host %q", buildkitHost)
+ log.L.Debugf("Chosen buildkit host %q", buildkitHost)
return buildkitHost, nil
}
- allErr = multierror.Append(allErr, fmt.Errorf("failed to ping to host %s: %w", buildkitHost, err))
+ errs = append(errs, fmt.Errorf("failed to ping to host %s: %w", buildkitHost, err))
}
- logrus.WithError(allErr).Error(getHint())
- return "", fmt.Errorf("no buildkit host is available, tried %d candidates: %w", len(hostRel), allErr)
+ allErr := errors.Join(errs...)
+ log.L.WithError(allErr).Error(getHint())
+ return "", fmt.Errorf("no buildkit host is available, tried %d candidates: %w", len(paths), allErr)
}
func GetWorkerLabels(buildkitHost string) (labels map[string]string, _ error) {
@@ -136,7 +125,7 @@ func getHint() string {
func PingBKDaemon(buildkitHost string) error {
if out, err := pingBKDaemon(buildkitHost); err != nil {
if out != "" {
- logrus.Error(out)
+ log.L.Error(out)
}
return fmt.Errorf(getHint()+": %w", err)
}
@@ -144,8 +133,9 @@ func PingBKDaemon(buildkitHost string) error {
}
func pingBKDaemon(buildkitHost string) (output string, _ error) {
- if runtime.GOOS != "linux" {
- return "", errors.New("only linux is supported")
+ supportedOses := []string{"linux", "freebsd", "windows"}
+ if !slices.Contains(supportedOses, runtime.GOOS) {
+ return "", fmt.Errorf("only %s are supported", strings.Join(supportedOses, ", "))
}
buildctlBinary, err := BuildctlBinary()
if err != nil {
@@ -185,7 +175,7 @@ func WriteTempDockerfile(rc io.Reader) (dockerfileDir string, err error) {
return dockerfileDir, nil
}
-// Buildkit file returns the values for the following buildctl args
+// BuildKitFile returns the values for the following buildctl args
// --localfilename=dockerfile={absDir}
// --opt=filename={file}
func BuildKitFile(dir, inputfile string) (absDir string, file string, err error) {
@@ -215,7 +205,7 @@ func BuildKitFile(dir, inputfile string) (absDir string, file string, err error)
return "", "", err
}
if !bytes.Equal(dockerfile, containerfile) {
- logrus.Warnf("%s and %s have different contents, building with %s", DefaultDockerfileName, ContainerfileName, DefaultDockerfileName)
+ log.L.Warnf("%s and %s have different contents, building with %s", DefaultDockerfileName, ContainerfileName, DefaultDockerfileName)
}
}
if dErr != nil {
diff --git a/pkg/buildkitutil/buildkitutil_freebsd.go b/pkg/buildkitutil/buildkitutil_freebsd.go
new file mode 100644
index 00000000000..adaec5e42ef
--- /dev/null
+++ b/pkg/buildkitutil/buildkitutil_freebsd.go
@@ -0,0 +1,22 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package buildkitutil
+
+func getRuntimeVariableDataDir() string {
+ // Per hier(7) dated July 6, 2023.
+ return "/var/run"
+}
diff --git a/cmd/nerdctl/volume_remove_linux_test.go b/pkg/buildkitutil/buildkitutil_linux.go
similarity index 55%
rename from cmd/nerdctl/volume_remove_linux_test.go
rename to pkg/buildkitutil/buildkitutil_linux.go
index b244a5101e2..dad13d022f2 100644
--- a/cmd/nerdctl/volume_remove_linux_test.go
+++ b/pkg/buildkitutil/buildkitutil_linux.go
@@ -14,25 +14,27 @@
limitations under the License.
*/
-package main
+package buildkitutil
import (
"fmt"
- "testing"
- "github.com/containerd/nerdctl/pkg/testutil"
-)
-
-func TestVolumeRemove(t *testing.T) {
- t.Parallel()
- base := testutil.NewBase(t)
- tID := testutil.Identifier(t)
+ "github.com/containerd/log"
- base.Cmd("volume", "create", tID).AssertOK()
- base.Cmd("run", "-v", fmt.Sprintf("%s:/volume", tID), "--name", tID, testutil.CommonImage).AssertOK()
- defer base.Cmd("rm", "-f", tID).Run()
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+)
- base.Cmd("volume", "rm", tID).AssertFail()
- base.Cmd("rm", "-f", tID).AssertOK()
- base.Cmd("volume", "rm", tID).AssertOK()
+func getRuntimeVariableDataDir() string {
+ // Per Linux Foundation "Filesystem Hierarchy Standard" version 3.0 section 3.15.
+ // Under version 2.3, this was "/var/run".
+ run := "/run"
+ if rootlessutil.IsRootless() {
+ var err error
+ run, err = rootlessutil.XDGRuntimeDir()
+ if err != nil {
+ log.L.Warn(err)
+ run = fmt.Sprintf("/run/user/%d", rootlessutil.ParentEUID())
+ }
+ }
+ return run
}
diff --git a/pkg/buildkitutil/buildkitutil_unix.go b/pkg/buildkitutil/buildkitutil_unix.go
new file mode 100644
index 00000000000..5c5498d3aa9
--- /dev/null
+++ b/pkg/buildkitutil/buildkitutil_unix.go
@@ -0,0 +1,39 @@
+//go:build unix
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package buildkitutil
+
+import (
+ "fmt"
+ "path/filepath"
+)
+
+func getBuildkitHostCandidates(namespace string) ([]string, error) {
+ if namespace == "" {
+ return []string{}, fmt.Errorf("namespace must be specified")
+ }
+ // Try candidate locations of the current containerd namespace.
+ run := getRuntimeVariableDataDir()
+ var candidates []string
+ if namespace != "default" {
+ candidates = append(candidates, "unix://"+filepath.Join(run, fmt.Sprintf("buildkit-%s/buildkitd.sock", namespace)))
+ }
+ candidates = append(candidates, "unix://"+filepath.Join(run, "buildkit-default/buildkitd.sock"), "unix://"+filepath.Join(run, "buildkit/buildkitd.sock"))
+
+ return candidates, nil
+}
diff --git a/pkg/buildkitutil/buildkitutil_windows.go b/pkg/buildkitutil/buildkitutil_windows.go
new file mode 100644
index 00000000000..dd38470c068
--- /dev/null
+++ b/pkg/buildkitutil/buildkitutil_windows.go
@@ -0,0 +1,21 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package buildkitutil
+
+func getBuildkitHostCandidates(namespace string) ([]string, error) {
+ return []string{"npipe:////./pipe/buildkitd"}, nil
+}
diff --git a/pkg/bypass4netnsutil/bypass.go b/pkg/bypass4netnsutil/bypass.go
index b5497d3bba8..bc9eed11f9d 100644
--- a/pkg/bypass4netnsutil/bypass.go
+++ b/pkg/bypass4netnsutil/bypass.go
@@ -18,34 +18,55 @@ package bypass4netnsutil
import (
"context"
+ "encoding/json"
"fmt"
"net"
"path/filepath"
- "github.com/containerd/containerd/errdefs"
- gocni "github.com/containerd/go-cni"
b4nnapi "github.com/rootless-containers/bypass4netns/pkg/api"
"github.com/rootless-containers/bypass4netns/pkg/api/daemon/client"
- rlkclient "github.com/rootless-containers/rootlesskit/pkg/api/client"
+ rlkclient "github.com/rootless-containers/rootlesskit/v2/pkg/api/client"
+
+ "github.com/containerd/errdefs"
+ "github.com/containerd/go-cni"
+
+ "github.com/containerd/nerdctl/v2/pkg/annotations"
)
-func NewBypass4netnsCNIBypassManager(client client.Client, rlkClient rlkclient.Client) (*Bypass4netnsCNIBypassManager, error) {
+func NewBypass4netnsCNIBypassManager(client client.Client, rlkClient rlkclient.Client, annotationsMap map[string]string) (*Bypass4netnsCNIBypassManager, error) {
if client == nil || rlkClient == nil {
return nil, errdefs.ErrInvalidArgument
}
+ enabled, bindEnabled, err := IsBypass4netnsEnabled(annotationsMap)
+ if err != nil {
+ return nil, err
+ }
+ if !enabled {
+ return nil, errdefs.ErrInvalidArgument
+ }
+ var ignoreSubnets []string
+ if v := annotationsMap[annotations.Bypass4netnsIgnoreSubnets]; v != "" {
+ if err := json.Unmarshal([]byte(v), &ignoreSubnets); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal annotation %q: %q: %w", annotations.Bypass4netnsIgnoreSubnets, v, err)
+ }
+ }
pm := &Bypass4netnsCNIBypassManager{
- Client: client,
- rlkClient: rlkClient,
+ Client: client,
+ rlkClient: rlkClient,
+ ignoreSubnets: ignoreSubnets,
+ ignoreBind: !bindEnabled,
}
return pm, nil
}
type Bypass4netnsCNIBypassManager struct {
client.Client
- rlkClient rlkclient.Client
+ rlkClient rlkclient.Client
+ ignoreSubnets []string
+ ignoreBind bool
}
-func (b4nnm *Bypass4netnsCNIBypassManager) StartBypass(ctx context.Context, ports []gocni.PortMapping, id, stateDir string) error {
+func (b4nnm *Bypass4netnsCNIBypassManager) StartBypass(ctx context.Context, ports []cni.PortMapping, id, stateDir string) error {
socketPath, err := GetSocketPathByID(id)
if err != nil {
return err
@@ -73,18 +94,21 @@ func (b4nnm *Bypass4netnsCNIBypassManager) StartBypass(ctx context.Context, port
PidFilePath: pidFilePath,
LogFilePath: logFilePath,
// "auto" can detect CNI CIDRs automatically
- IgnoreSubnets: []string{"127.0.0.0/8", rlkCIDR, "auto"},
+ IgnoreSubnets: append([]string{"127.0.0.0/8", rlkCIDR, "auto"}, b4nnm.ignoreSubnets...),
+ IgnoreBind: b4nnm.ignoreBind,
}
- portMap := []b4nnapi.PortSpec{}
- for _, p := range ports {
- portMap = append(portMap, b4nnapi.PortSpec{
- ParentIP: p.HostIP,
- ParentPort: int(p.HostPort),
- ChildPort: int(p.ContainerPort),
- Protos: []string{p.Protocol},
- })
+ if !b4nnm.ignoreBind {
+ portMap := []b4nnapi.PortSpec{}
+ for _, p := range ports {
+ portMap = append(portMap, b4nnapi.PortSpec{
+ ParentIP: p.HostIP,
+ ParentPort: int(p.HostPort),
+ ChildPort: int(p.ContainerPort),
+ Protos: []string{p.Protocol},
+ })
+ }
+ spec.PortMapping = portMap
}
- spec.PortMapping = portMap
_, err = b4nnm.BypassManager().StartBypass(ctx, spec)
if err != nil {
return err
diff --git a/pkg/bypass4netnsutil/bypass4netnsutil.go b/pkg/bypass4netnsutil/bypass4netnsutil.go
index 4e2dfa826e3..b34f1e4c08f 100644
--- a/pkg/bypass4netnsutil/bypass4netnsutil.go
+++ b/pkg/bypass4netnsutil/bypass4netnsutil.go
@@ -23,11 +23,13 @@ import (
"path/filepath"
"strconv"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/labels"
"github.com/opencontainers/runtime-spec/specs-go"
b4nnoci "github.com/rootless-containers/bypass4netns/pkg/oci"
+
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/annotations"
)
func generateSecurityOpt(listenerPath string) (oci.SpecOpts, error) {
@@ -46,8 +48,8 @@ func generateSecurityOpt(listenerPath string) (oci.SpecOpts, error) {
return opt, nil
}
-func GenerateBypass4netnsOpts(securityOptsMaps map[string]string, labelMaps map[string]string, id string) ([]oci.SpecOpts, error) {
- b4nn, ok := labelMaps[labels.Bypass4netns]
+func GenerateBypass4netnsOpts(securityOptsMaps map[string]string, annotationsMap map[string]string, id string) ([]oci.SpecOpts, error) {
+ b4nn, ok := annotationsMap[annotations.Bypass4netns]
if !ok {
return nil, nil
}
@@ -129,19 +131,31 @@ func GetPidFilePathByID(id string) (string, error) {
return "", err
}
- socketPath := filepath.Join(xdgRuntimeDir, "bypass4netns", id[0:15]+".pid")
- return socketPath, nil
+ pidPath := filepath.Join(xdgRuntimeDir, "bypass4netns", id[0:15]+".pid")
+
+ err = os.MkdirAll(filepath.Join(xdgRuntimeDir, "bypass4netns"), 0o700)
+ if err != nil {
+ return "", err
+ }
+
+ return pidPath, nil
}
-func IsBypass4netnsEnabled(annotations map[string]string) (bool, error) {
- if b4nn, ok := annotations[labels.Bypass4netns]; ok {
- b4nnEnable, err := strconv.ParseBool(b4nn)
+func IsBypass4netnsEnabled(annotationsMap map[string]string) (enabled, bindEnabled bool, err error) {
+ if b4nn, ok := annotationsMap[annotations.Bypass4netns]; ok {
+ enabled, err = strconv.ParseBool(b4nn)
if err != nil {
- return false, err
+ return
+ }
+ bindEnabled = enabled
+ if s, ok := annotationsMap[annotations.Bypass4netnsIgnoreBind]; ok {
+ var bindDisabled bool
+ bindDisabled, err = strconv.ParseBool(s)
+ if err != nil {
+ return
+ }
+ bindEnabled = !bindDisabled
}
-
- return b4nnEnable, nil
}
-
- return false, nil
+ return
}
diff --git a/pkg/cioutil/container_io.go b/pkg/cioutil/container_io.go
new file mode 100644
index 00000000000..1b0b1a01bfb
--- /dev/null
+++ b/pkg/cioutil/container_io.go
@@ -0,0 +1,221 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package cioutil
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net/url"
+ "os"
+ "os/exec"
+ "runtime"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/containerd/containerd/v2/cmd/containerd-shim-runc-v2/process"
+ "github.com/containerd/containerd/v2/defaults"
+ "github.com/containerd/containerd/v2/pkg/cio"
+)
+
+const binaryIOProcTermTimeout = 12 * time.Second // Give logger process 10 seconds for cleanup
+
+// ncio is a basic container IO implementation.
+type ncio struct {
+ cmd *exec.Cmd
+ config cio.Config
+ wg *sync.WaitGroup
+ closers []io.Closer
+ cancel context.CancelFunc
+}
+
+var bufPool = sync.Pool{
+ New: func() interface{} {
+ buffer := make([]byte, 32<<10)
+ return &buffer
+ },
+}
+
+func (c *ncio) Config() cio.Config {
+ return c.config
+}
+
+func (c *ncio) Wait() {
+ if c.wg != nil {
+ c.wg.Wait()
+ }
+}
+
+func (c *ncio) Close() error {
+
+ var lastErr error
+
+ if c.cmd != nil && c.cmd.Process != nil {
+
+ // Send SIGTERM first, so logger process has a chance to flush and exit properly
+ if err := c.cmd.Process.Signal(syscall.SIGTERM); err != nil {
+ lastErr = fmt.Errorf("failed to send SIGTERM: %w", err)
+
+ if err := c.cmd.Process.Kill(); err != nil {
+ lastErr = errors.Join(lastErr, fmt.Errorf("failed to kill process after faulty SIGTERM: %w", err))
+ }
+
+ }
+
+ done := make(chan error, 1)
+ go func() {
+ done <- c.cmd.Wait()
+ }()
+
+ select {
+ case err := <-done:
+ return err
+ case <-time.After(binaryIOProcTermTimeout):
+
+ err := c.cmd.Process.Kill()
+ if err != nil {
+ lastErr = fmt.Errorf("failed to kill shim logger process: %w", err)
+ }
+
+ }
+ }
+
+ for _, closer := range c.closers {
+ if closer == nil {
+ continue
+ }
+ if err := closer.Close(); err != nil {
+ lastErr = err
+ }
+ }
+ return lastErr
+}
+
+func (c *ncio) Cancel() {
+ if c.cancel != nil {
+ c.cancel()
+ }
+}
+
+func NewContainerIO(namespace string, logURI string, tty bool, stdin io.Reader, stdout, stderr io.Writer) cio.Creator {
+ return func(id string) (_ cio.IO, err error) {
+ var (
+ cmd *exec.Cmd
+ closers []func() error
+ streams = &cio.Streams{
+ Terminal: tty,
+ }
+ )
+
+ defer func() {
+ if err == nil {
+ return
+ }
+ result := []error{err}
+ for _, fn := range closers {
+ result = append(result, fn())
+ }
+ err = errors.Join(result...)
+ }()
+
+ if stdin != nil {
+ streams.Stdin = stdin
+ }
+
+ var stdoutWriters []io.Writer
+ if stdout != nil {
+ stdoutWriters = append(stdoutWriters, stdout)
+ }
+
+ var stderrWriters []io.Writer
+ if stderr != nil {
+ stderrWriters = append(stderrWriters, stderr)
+ }
+
+ if runtime.GOOS != "windows" {
+ // starting logging binary logic is from https://github.com/containerd/containerd/blob/194a1fdd2cde35bc019ef138f30485e27fe0913e/cmd/containerd-shim-runc-v2/process/io.go#L247
+ stdoutr, stdoutw, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ closers = append(closers, stdoutr.Close, stdoutw.Close)
+
+ stderrr, stderrw, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ closers = append(closers, stderrr.Close, stderrw.Close)
+
+ r, w, err := os.Pipe()
+ if err != nil {
+ return nil, err
+ }
+ closers = append(closers, r.Close, w.Close)
+
+ u, err := url.Parse(logURI)
+ if err != nil {
+ return nil, err
+ }
+ cmd = process.NewBinaryCmd(u, id, namespace)
+ cmd.ExtraFiles = append(cmd.ExtraFiles, stdoutr, stderrr, w)
+
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("failed to start binary process with cmdArgs %v: %w", cmd.Args, err)
+ }
+
+ closers = append(closers, func() error { return cmd.Process.Kill() })
+
+ // close our side of the pipe after start
+ if err := w.Close(); err != nil {
+ return nil, fmt.Errorf("failed to close write pipe after start: %w", err)
+ }
+
+ // wait for the logging binary to be ready
+ b := make([]byte, 1)
+ if _, err := r.Read(b); err != nil && err != io.EOF {
+ return nil, fmt.Errorf("failed to read from logging binary: %w", err)
+ }
+
+ stdoutWriters = append(stdoutWriters, stdoutw)
+ stderrWriters = append(stderrWriters, stderrw)
+ }
+
+ streams.Stdout = io.MultiWriter(stdoutWriters...)
+ streams.Stderr = io.MultiWriter(stderrWriters...)
+
+ if streams.FIFODir == "" {
+ streams.FIFODir = defaults.DefaultFIFODir
+ }
+ fifos, err := cio.NewFIFOSetInDir(streams.FIFODir, id, streams.Terminal)
+ if err != nil {
+ return nil, err
+ }
+
+ if streams.Stdin == nil {
+ fifos.Stdin = ""
+ }
+ if streams.Stdout == nil {
+ fifos.Stdout = ""
+ }
+ if streams.Stderr == nil {
+ fifos.Stderr = ""
+ }
+ return copyIO(cmd, fifos, streams)
+ }
+}
diff --git a/pkg/cioutil/container_io_unix.go b/pkg/cioutil/container_io_unix.go
new file mode 100644
index 00000000000..c9d23110940
--- /dev/null
+++ b/pkg/cioutil/container_io_unix.go
@@ -0,0 +1,136 @@
+//go:build unix
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package cioutil
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os/exec"
+ "sync"
+ "syscall"
+
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/fifo"
+)
+
+type pipes struct {
+ Stdin io.WriteCloser
+ Stdout io.ReadCloser
+ Stderr io.ReadCloser
+}
+
+func (p *pipes) closers() []io.Closer {
+ return []io.Closer{p.Stdin, p.Stdout, p.Stderr}
+}
+
+// copyIO is from https://github.com/containerd/containerd/blob/148d21b1ae0718b75718a09ecb307bb874270f59/cio/io_unix.go#L55
+func copyIO(cmd *exec.Cmd, fifos *cio.FIFOSet, ioset *cio.Streams) (*ncio, error) {
+ var ctx, cancel = context.WithCancel(context.Background())
+ pipes, err := openFifos(ctx, fifos)
+ if err != nil {
+ cancel()
+ return nil, err
+ }
+
+ if fifos.Stdin != "" {
+ go func() {
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(pipes.Stdin, ioset.Stdin, *p)
+ pipes.Stdin.Close()
+ }()
+ }
+
+ var wg = &sync.WaitGroup{}
+ if fifos.Stdout != "" {
+ wg.Add(1)
+ go func() {
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(ioset.Stdout, pipes.Stdout, *p)
+ pipes.Stdout.Close()
+ wg.Done()
+ }()
+ }
+
+ if !fifos.Terminal && fifos.Stderr != "" {
+ wg.Add(1)
+ go func() {
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(ioset.Stderr, pipes.Stderr, *p)
+ pipes.Stderr.Close()
+ wg.Done()
+ }()
+ }
+
+ return &ncio{
+ cmd: cmd,
+ config: fifos.Config,
+ wg: wg,
+ closers: append(pipes.closers(), fifos),
+ cancel: func() {
+ cancel()
+ for _, c := range pipes.closers() {
+ if c != nil {
+ c.Close()
+ }
+ }
+ },
+ }, nil
+}
+
+func openFifos(ctx context.Context, fifos *cio.FIFOSet) (f pipes, retErr error) {
+ defer func() {
+ if retErr != nil {
+ fifos.Close()
+ }
+ }()
+
+ if fifos.Stdin != "" {
+ if f.Stdin, retErr = fifo.OpenFifo(ctx, fifos.Stdin, syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); retErr != nil {
+ return f, fmt.Errorf("failed to open stdin fifo: %w", retErr)
+ }
+ defer func() {
+ if retErr != nil && f.Stdin != nil {
+ f.Stdin.Close()
+ }
+ }()
+ }
+ if fifos.Stdout != "" {
+ if f.Stdout, retErr = fifo.OpenFifo(ctx, fifos.Stdout, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); retErr != nil {
+ return f, fmt.Errorf("failed to open stdout fifo: %w", retErr)
+ }
+ defer func() {
+ if retErr != nil && f.Stdout != nil {
+ f.Stdout.Close()
+ }
+ }()
+ }
+ if !fifos.Terminal && fifos.Stderr != "" {
+ if f.Stderr, retErr = fifo.OpenFifo(ctx, fifos.Stderr, syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0700); retErr != nil {
+ return f, fmt.Errorf("failed to open stderr fifo: %w", retErr)
+ }
+ }
+ return f, nil
+}
diff --git a/pkg/cioutil/container_io_windows.go b/pkg/cioutil/container_io_windows.go
new file mode 100644
index 00000000000..4d9789e0763
--- /dev/null
+++ b/pkg/cioutil/container_io_windows.go
@@ -0,0 +1,110 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package cioutil
+
+import (
+ "fmt"
+ "io"
+ "os/exec"
+
+ "github.com/Microsoft/go-winio"
+
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/log"
+)
+
+// copyIO is from https://github.com/containerd/containerd/blob/148d21b1ae0718b75718a09ecb307bb874270f59/cio/io_windows.go#L44
+func copyIO(_ *exec.Cmd, fifos *cio.FIFOSet, ioset *cio.Streams) (_ *ncio, retErr error) {
+ ncios := &ncio{cmd: nil, config: fifos.Config}
+
+ defer func() {
+ if retErr != nil {
+ _ = ncios.Close()
+ }
+ }()
+
+ if fifos.Stdin != "" {
+ l, err := winio.ListenPipe(fifos.Stdin, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create stdin pipe %s: %w", fifos.Stdin, err)
+ }
+ ncios.closers = append(ncios.closers, l)
+
+ go func() {
+ c, err := l.Accept()
+ if err != nil {
+ log.L.WithError(err).Errorf("failed to accept stdin connection on %s", fifos.Stdin)
+ return
+ }
+
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(c, ioset.Stdin, *p)
+ c.Close()
+ l.Close()
+ }()
+ }
+
+ if fifos.Stdout != "" {
+ l, err := winio.ListenPipe(fifos.Stdout, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create stdout pipe %s: %w", fifos.Stdout, err)
+ }
+ ncios.closers = append(ncios.closers, l)
+
+ go func() {
+ c, err := l.Accept()
+ if err != nil {
+ log.L.WithError(err).Errorf("failed to accept stdout connection on %s", fifos.Stdout)
+ return
+ }
+
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(ioset.Stdout, c, *p)
+ c.Close()
+ l.Close()
+ }()
+ }
+
+ if fifos.Stderr != "" {
+ l, err := winio.ListenPipe(fifos.Stderr, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create stderr pipe %s: %w", fifos.Stderr, err)
+ }
+ ncios.closers = append(ncios.closers, l)
+
+ go func() {
+ c, err := l.Accept()
+ if err != nil {
+ log.L.WithError(err).Errorf("failed to accept stderr connection on %s", fifos.Stderr)
+ return
+ }
+
+ p := bufPool.Get().(*[]byte)
+ defer bufPool.Put(p)
+
+ io.CopyBuffer(ioset.Stderr, c, *p)
+ c.Close()
+ l.Close()
+ }()
+ }
+
+ return ncios, nil
+}
diff --git a/pkg/clientutil/client.go b/pkg/clientutil/client.go
index 45013133ad5..7304f7a44ec 100644
--- a/pkg/clientutil/client.go
+++ b/pkg/clientutil/client.go
@@ -24,17 +24,18 @@ import (
"runtime"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/namespaces"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/systemutil"
"github.com/opencontainers/go-digest"
- "github.com/sirupsen/logrus"
-)
-func NewClient(ctx context.Context, namespace, address string, opts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) {
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/systemutil"
+)
+func NewClient(ctx context.Context, namespace, address string, opts ...containerd.Opt) (*containerd.Client, context.Context, context.CancelFunc, error) {
ctx = namespaces.WithNamespace(ctx, namespace)
address = strings.TrimPrefix(address, "unix://")
@@ -56,15 +57,15 @@ func NewClient(ctx context.Context, namespace, address string, opts ...container
return client, ctx, cancel, nil
}
-func NewClientWithPlatform(ctx context.Context, namespace, address, platform string, clientOpts ...containerd.ClientOpt) (*containerd.Client, context.Context, context.CancelFunc, error) {
+func NewClientWithPlatform(ctx context.Context, namespace, address, platform string, clientOpts ...containerd.Opt) (*containerd.Client, context.Context, context.CancelFunc, error) {
if platform != "" {
if canExec, canExecErr := platformutil.CanExecProbably(platform); !canExec {
warn := fmt.Sprintf("Platform %q seems incompatible with the host platform %q. If you see \"exec format error\", see https://github.com/containerd/nerdctl/blob/main/docs/multi-platform.md",
platform, platforms.DefaultString())
if canExecErr != nil {
- logrus.WithError(canExecErr).Warn(warn)
+ log.L.WithError(canExecErr).Warn(warn)
} else {
- logrus.Warn(warn)
+ log.L.Warn(warn)
}
}
platformParsed, err := platforms.Parse(platform)
diff --git a/pkg/cmd/apparmor/inspect_linux.go b/pkg/cmd/apparmor/inspect_linux.go
index 5fa2a17286d..67b067b532f 100644
--- a/pkg/cmd/apparmor/inspect_linux.go
+++ b/pkg/cmd/apparmor/inspect_linux.go
@@ -19,9 +19,10 @@ package apparmor
import (
"fmt"
- "github.com/containerd/containerd/contrib/apparmor"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/defaults"
+ "github.com/containerd/containerd/v2/contrib/apparmor"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
)
func Inspect(options types.ApparmorInspectOptions) error {
diff --git a/pkg/cmd/apparmor/list_linux.go b/pkg/cmd/apparmor/list_linux.go
index c7a03b3798b..b5825545159 100644
--- a/pkg/cmd/apparmor/list_linux.go
+++ b/pkg/cmd/apparmor/list_linux.go
@@ -23,9 +23,9 @@ import (
"text/tabwriter"
"text/template"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/apparmorutil"
- "github.com/containerd/nerdctl/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
)
func List(options types.ApparmorListOptions) error {
@@ -63,7 +63,7 @@ func List(options types.ApparmorListOptions) error {
if err := tmpl.Execute(&b, f); err != nil {
return err
}
- if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil {
+ if _, err = fmt.Fprintln(w, b.String()); err != nil {
return err
}
} else if quiet {
diff --git a/pkg/cmd/apparmor/load_linux.go b/pkg/cmd/apparmor/load_linux.go
index b8112aa99e0..f5930379a6c 100644
--- a/pkg/cmd/apparmor/load_linux.go
+++ b/pkg/cmd/apparmor/load_linux.go
@@ -17,12 +17,13 @@
package apparmor
import (
- "github.com/containerd/containerd/contrib/apparmor"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/contrib/apparmor"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
)
func Load() error {
- logrus.Infof("Loading profile %q", defaults.AppArmorProfileName)
+ log.L.Infof("Loading profile %q", defaults.AppArmorProfileName)
return apparmor.LoadDefaultProfile(defaults.AppArmorProfileName)
}
diff --git a/pkg/cmd/apparmor/unload_linux.go b/pkg/cmd/apparmor/unload_linux.go
index 8994834efdf..f9ab7779f0d 100644
--- a/pkg/cmd/apparmor/unload_linux.go
+++ b/pkg/cmd/apparmor/unload_linux.go
@@ -17,11 +17,12 @@
package apparmor
import (
- "github.com/containerd/nerdctl/pkg/apparmorutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
)
func Unload(target string) error {
- logrus.Infof("Unloading profile %q", target)
+ log.L.Infof("Unloading profile %q", target)
return apparmorutil.Unload(target)
}
diff --git a/pkg/cmd/builder/build.go b/pkg/cmd/builder/build.go
index 84c5ff43b28..2ab6df0e8cb 100644
--- a/pkg/cmd/builder/build.go
+++ b/pkg/cmd/builder/build.go
@@ -28,20 +28,39 @@ import (
"strconv"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/images/archive"
- "github.com/containerd/containerd/platforms"
- dockerreference "github.com/containerd/containerd/reference/docker"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/sirupsen/logrus"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/archive"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/buildkitutil"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
+type PlatformParser interface {
+ Parse(platform string) (platforms.Platform, error)
+ DefaultSpec() platforms.Platform
+}
+
+type platformParser struct{}
+
+func (p platformParser) Parse(platform string) (platforms.Platform, error) {
+ return platforms.Parse(platform)
+}
+
+func (p platformParser) DefaultSpec() platforms.Platform {
+ return platforms.DefaultSpec()
+}
+
func Build(ctx context.Context, client *containerd.Client, options types.BuilderBuildOptions) error {
buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, err := generateBuildctlArgs(ctx, client, options)
if err != nil {
@@ -51,7 +70,7 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
defer cleanup()
}
- logrus.Debugf("running %s %v", buildctlBinary, buildctlArgs)
+ log.L.Debugf("running %s %v", buildctlBinary, buildctlArgs)
buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
buildctlCmd.Env = os.Environ()
@@ -91,13 +110,13 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
if err != nil {
return err
}
- if err := os.WriteFile(options.IidFile, []byte(id), 0600); err != nil {
+ if err := os.WriteFile(options.IidFile, []byte(id), 0644); err != nil {
return err
}
}
if len(tags) > 1 {
- logrus.Debug("Found more than 1 tag")
+ log.L.Debug("Found more than 1 tag")
imageService := client.ImageService()
image, err := imageService.Get(ctx, tags[0])
if err != nil {
@@ -108,6 +127,12 @@ func Build(ctx context.Context, client *containerd.Client, options types.Builder
if _, err := imageService.Create(ctx, image); err != nil {
// if already exists; skip.
if errors.Is(err, errdefs.ErrAlreadyExists) {
+ if err = imageService.Delete(ctx, targetRef); err != nil {
+ return err
+ }
+ if _, err = imageService.Create(ctx, image); err != nil {
+ return err
+ }
continue
}
return fmt.Errorf("unable to tag image: %s", err)
@@ -131,8 +156,10 @@ func loadImage(ctx context.Context, in io.Reader, namespace, address, snapshotte
if err != nil {
return err
}
- defer cancel()
-
+ defer func() {
+ cancel()
+ client.Close()
+ }()
r := &readCounter{Reader: in}
imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC))
if err != nil {
@@ -202,24 +229,26 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
output = fmt.Sprintf("type=local,dest=%s", output)
}
if strings.Contains(output, "type=docker") || strings.Contains(output, "type=oci") {
- needsLoading = true
+ if !strings.Contains(output, "dest=") {
+ needsLoading = true
+ }
}
}
if tags = strutil.DedupeStrSlice(options.Tag); len(tags) > 0 {
ref := tags[0]
- named, err := dockerreference.ParseNormalizedNamed(ref)
+ parsedReference, err := referenceutil.Parse(ref)
if err != nil {
return "", nil, false, "", nil, nil, err
}
- output += ",name=" + dockerreference.TagNameOnly(named).String()
+ output += ",name=" + parsedReference.String()
// pick the first tag and add it to output
for idx, tag := range tags {
- named, err := dockerreference.ParseNormalizedNamed(tag)
+ parsedReference, err = referenceutil.Parse(tag)
if err != nil {
return "", nil, false, "", nil, nil, err
}
- tags[idx] = dockerreference.TagNameOnly(named).String()
+ tags[idx] = parsedReference.String()
}
} else if len(tags) == 0 {
output = output + ",dangling-name-prefix="
@@ -261,6 +290,38 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
return "", nil, false, "", nil, nil, err
}
+ buildCtx, err := parseContextNames(options.ExtendedBuildContext)
+ if err != nil {
+ return "", nil, false, "", nil, nil, err
+ }
+
+ for k, v := range buildCtx {
+ isURL := strings.HasPrefix(v, "https://") || strings.HasPrefix(v, "http://")
+ isDockerImage := strings.HasPrefix(v, "docker-image://") || strings.HasPrefix(v, "target:")
+
+ if isURL || isDockerImage {
+ buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=context:%s=%s", k, v))
+ continue
+ }
+
+ if isOCILayout := strings.HasPrefix(v, "oci-layout://"); isOCILayout {
+ args, err := parseBuildContextFromOCILayout(k, v)
+ if err != nil {
+ return "", nil, false, "", nil, nil, err
+ }
+
+ buildctlArgs = append(buildctlArgs, args...)
+ continue
+ }
+
+ path, err := filepath.Abs(v)
+ if err != nil {
+ return "", nil, false, "", nil, nil, err
+ }
+ buildctlArgs = append(buildctlArgs, fmt.Sprintf("--local=%s=%s", k, path))
+ buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=context:%s=local:%s", k, k))
+ }
+
buildctlArgs = append(buildctlArgs, "--local=dockerfile="+dir)
buildctlArgs = append(buildctlArgs, "--opt=filename="+file)
@@ -283,7 +344,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
if ok {
buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=build-arg:%s=%s", ba, val))
} else {
- logrus.Debugf("ignoring unset build arg %q", ba)
+ log.L.Debugf("ignoring unset build arg %q", ba)
}
} else if len(arr) > 1 && len(arr[0]) > 0 {
buildctlArgs = append(buildctlArgs, "--opt=build-arg:"+ba)
@@ -298,7 +359,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
buildctlArgs = append(buildctlArgs, "--export-cache=type=inline")
}
} else {
- logrus.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic)
+ log.L.WithError(err).Warnf("invalid BUILDKIT_INLINE_CACHE: %q", bic)
}
}
} else {
@@ -322,10 +383,33 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
buildctlArgs = append(buildctlArgs, "--no-cache")
}
+ if options.Pull != nil {
+ switch *options.Pull {
+ case true:
+ buildctlArgs = append(buildctlArgs, "--opt=image-resolve-mode=pull")
+ case false:
+ buildctlArgs = append(buildctlArgs, "--opt=image-resolve-mode=local")
+ }
+ }
+
for _, s := range strutil.DedupeStrSlice(options.Secret) {
buildctlArgs = append(buildctlArgs, "--secret="+s)
}
+ for _, s := range strutil.DedupeStrSlice(options.Allow) {
+ buildctlArgs = append(buildctlArgs, "--allow="+s)
+ }
+
+ for _, s := range strutil.DedupeStrSlice(options.Attest) {
+ optAttestType, optAttestAttrs, _ := strings.Cut(s, ",")
+ if strings.HasPrefix(optAttestType, "type=") {
+ optAttestType := strings.TrimPrefix(optAttestType, "type=")
+ buildctlArgs = append(buildctlArgs, fmt.Sprintf("--opt=attest:%s=%s", optAttestType, optAttestAttrs))
+ } else {
+ return "", nil, false, "", nil, nil, fmt.Errorf("attestation type not specified")
+ }
+ }
+
for _, s := range strutil.DedupeStrSlice(options.SSH) {
buildctlArgs = append(buildctlArgs, "--ssh="+s)
}
@@ -345,7 +429,7 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
}
if !options.Rm {
- logrus.Warn("ignoring deprecated flag: '--rm=false'")
+ log.L.Warn("ignoring deprecated flag: '--rm=false'")
}
if options.IidFile != "" {
@@ -358,6 +442,26 @@ func generateBuildctlArgs(ctx context.Context, client *containerd.Client, option
buildctlArgs = append(buildctlArgs, "--metadata-file="+metaFile)
}
+ if options.NetworkMode != "" {
+ switch options.NetworkMode {
+ case "none":
+ buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode)
+ case "host":
+ buildctlArgs = append(buildctlArgs, "--opt=force-network-mode="+options.NetworkMode, "--allow=network.host", "--allow=security.insecure")
+ case "", "default":
+ default:
+ log.L.Debugf("ignoring network build arg %s", options.NetworkMode)
+ }
+ }
+
+ if len(options.ExtraHosts) > 0 {
+ extraHosts, err := containerutil.ParseExtraHosts(options.ExtraHosts, options.GOptions.HostGatewayIP, "=")
+ if err != nil {
+ return "", nil, false, "", nil, nil, err
+ }
+ buildctlArgs = append(buildctlArgs, "--opt=add-hosts="+strings.Join(extraHosts, ","))
+ }
+
return buildctlBinary, buildctlArgs, needsLoading, metaFile, tags, cleanup, nil
}
@@ -370,7 +474,7 @@ func getDigestFromMetaFile(path string) (string, error) {
metadata := map[string]json.RawMessage{}
if err := json.Unmarshal(data, &metadata); err != nil {
- logrus.WithError(err).Errorf("failed to unmarshal metadata file %s", path)
+ log.L.WithError(err).Errorf("failed to unmarshal metadata file %s", path)
return "", err
}
digestRaw, ok := metadata["containerimage.digest"]
@@ -379,18 +483,41 @@ func getDigestFromMetaFile(path string) (string, error) {
}
var digest string
if err := json.Unmarshal(digestRaw, &digest); err != nil {
- logrus.WithError(err).Errorf("failed to unmarshal digset")
+ log.L.WithError(err).Errorf("failed to unmarshal digset")
return "", err
}
return digest, nil
}
+func isMatchingRuntimePlatform(platform string, parser PlatformParser) bool {
+ p, err := parser.Parse(platform)
+ if err != nil {
+ return false
+ }
+ d := parser.DefaultSpec()
+
+ if p.OS == d.OS && p.Architecture == d.Architecture && (p.Variant == "" || p.Variant == d.Variant) {
+ return true
+ }
+
+ return false
+}
+
+func isBuildPlatformDefault(platform []string, parser PlatformParser) bool {
+ if len(platform) == 0 {
+ return true
+ } else if len(platform) == 1 {
+ return isMatchingRuntimePlatform(platform[0], parser)
+ }
+ return false
+}
+
func isImageSharable(buildkitHost, namespace, uuid, snapshotter string, platform []string) (bool, error) {
labels, err := buildkitutil.GetWorkerLabels(buildkitHost)
if err != nil {
return false, err
}
- logrus.Debugf("worker labels: %+v", labels)
+ log.L.Debugf("worker labels: %+v", labels)
executor, ok := labels["org.mobyproject.buildkit.worker.executor"]
if !ok {
return false, nil
@@ -411,5 +538,79 @@ func isImageSharable(buildkitHost, namespace, uuid, snapshotter string, platform
// Dockerfile doesn't contain instructions require base images like RUN) even if `--output type=image,unpack=true`
// is passed to BuildKit. Thus, we need to use `type=docker` or `type=oci` when nerdctl builds non-default platform
// image using `platform` option.
- return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && len(platform) == 0, nil
+ parser := new(platformParser)
+ return executor == "containerd" && containerdUUID == uuid && containerdNamespace == namespace && workerSnapshotter == snapshotter && isBuildPlatformDefault(platform, parser), nil
+}
+
+func parseContextNames(values []string) (map[string]string, error) {
+ if len(values) == 0 {
+ return nil, nil
+ }
+ result := make(map[string]string, len(values))
+ for _, value := range values {
+ kv := strings.SplitN(value, "=", 2)
+ if len(kv) != 2 {
+ return nil, fmt.Errorf("invalid context value: %s, expected key=value", value)
+ }
+ result[kv[0]] = kv[1]
+ }
+ return result, nil
+}
+
+var (
+ ErrOCILayoutPrefixNotFound = errors.New("OCI layout prefix not found")
+ ErrOCILayoutEmptyDigest = errors.New("OCI layout cannot have empty digest")
+)
+
+func parseBuildContextFromOCILayout(name, path string) ([]string, error) {
+ path, found := strings.CutPrefix(path, "oci-layout://")
+ if !found {
+ return []string{}, ErrOCILayoutPrefixNotFound
+ }
+
+ abspath, err := filepath.Abs(path)
+ if err != nil {
+ return []string{}, err
+ }
+
+ ociIndex, err := readOCIIndexFromPath(abspath)
+ if err != nil {
+ return []string{}, err
+ }
+
+ var digest string
+ for _, manifest := range ociIndex.Manifests {
+ if images.IsManifestType(manifest.MediaType) {
+ digest = manifest.Digest.String()
+ }
+ }
+
+ if digest == "" {
+ return []string{}, ErrOCILayoutEmptyDigest
+ }
+
+ return []string{
+ fmt.Sprintf("--oci-layout=parent-image-key=%s", abspath),
+ fmt.Sprintf("--opt=context:%s=oci-layout:parent-image-key@%s", name, digest),
+ }, nil
+}
+
+func readOCIIndexFromPath(path string) (*ocispec.Index, error) {
+ ociIndexJSONFile, err := os.Open(filepath.Join(path, "index.json"))
+ if err != nil {
+ return nil, err
+ }
+ defer ociIndexJSONFile.Close()
+
+ rawBytes, err := io.ReadAll(ociIndexJSONFile)
+ if err != nil {
+ return nil, err
+ }
+
+ var ociIndex *ocispec.Index
+ err = json.Unmarshal(rawBytes, &ociIndex)
+ if err != nil {
+ return nil, err
+ }
+ return ociIndex, nil
}
diff --git a/pkg/cmd/builder/build_test.go b/pkg/cmd/builder/build_test.go
new file mode 100644
index 00000000000..081d899d7f3
--- /dev/null
+++ b/pkg/cmd/builder/build_test.go
@@ -0,0 +1,240 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package builder
+
+import (
+ "fmt"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "testing"
+
+ specs "github.com/opencontainers/image-spec/specs-go/v1"
+ "go.uber.org/mock/gomock"
+ "gotest.tools/v3/assert"
+)
+
+type MockParse struct {
+ ctrl *gomock.Controller
+ recorder *MockParseRecorder
+}
+
+type MockParseRecorder struct {
+ mock *MockParse
+}
+
+func newMockParser(ctrl *gomock.Controller) *MockParse {
+ mock := &MockParse{ctrl: ctrl}
+ mock.recorder = &MockParseRecorder{mock}
+ return mock
+}
+
+func (m *MockParse) EXPECT() *MockParseRecorder {
+ return m.recorder
+}
+
+func (m *MockParse) Parse(platform string) (specs.Platform, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "Parse")
+ ret0, _ := ret[0].(specs.Platform)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+func (m *MockParseRecorder) Parse(platform string) *gomock.Call {
+ m.mock.ctrl.T.Helper()
+ return m.mock.ctrl.RecordCallWithMethodType(m.mock, "Parse", reflect.TypeOf((*MockParse)(nil).Parse))
+}
+
+func (m *MockParse) DefaultSpec() specs.Platform {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DefaultSpec")
+ ret0, _ := ret[0].(specs.Platform)
+ return ret0
+}
+
+func (m *MockParseRecorder) DefaultSpec() *gomock.Call {
+ m.mock.ctrl.T.Helper()
+ return m.mock.ctrl.RecordCallWithMethodType(m.mock, "DefaultSpec", reflect.TypeOf((*MockParse)(nil).DefaultSpec))
+}
+
+func TestIsMatchingRuntimePlatform(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ mock func(*MockParse)
+ want bool
+ }{
+ {
+ name: "Image is shareable when Runtime and build platform match for os, arch and variant",
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: true,
+ },
+ {
+ name: "Image is shareable when Runtime and build platform match for os, arch. Variant is not defined",
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: ""}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: true,
+ },
+ {
+ name: "Image is not shareable when Runtime and build platform donot math OS",
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "OS", Architecture: "mockArch", Variant: ""}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: false,
+ },
+ {
+ name: "Image is not shareable when Runtime and build platform donot math Arch",
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "Arch", Variant: ""}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: false,
+ },
+ {
+ name: "Image is not shareable when Runtime and build platform donot math Variant",
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "Variant"}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ mockParser := newMockParser(ctrl)
+ tc.mock(mockParser)
+ r := isMatchingRuntimePlatform("test", mockParser)
+ assert.Equal(t, r, tc.want, tc.name)
+ })
+ }
+}
+
+func TestIsBuildPlatformDefault(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ mock func(*MockParse)
+ platform []string
+ want bool
+ }{
+ {
+ name: "Image is shreable when len of platform is 0",
+ platform: make([]string, 0),
+ want: true,
+ },
+ {
+ name: "Image is shareable when Runtime and build platform match for os, arch and variant",
+ platform: []string{"test"},
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: true,
+ },
+ {
+ name: "Image is not shareable when Runtime build platform dont match",
+ platform: []string{"test"},
+ mock: func(mockParser *MockParse) {
+ mockParser.EXPECT().Parse("test").Return(specs.Platform{OS: "OS", Architecture: "mockArch", Variant: "mockVariant"}, nil)
+ mockParser.EXPECT().DefaultSpec().Return(specs.Platform{OS: "mockOS", Architecture: "mockArch", Variant: "mockVariant"})
+ },
+ want: false,
+ },
+ {
+ name: "Image is not shareable when more than 2 platforms are passed",
+ platform: []string{"test1", "test2"},
+ want: false,
+ },
+ }
+
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ ctrl := gomock.NewController(t)
+ mockParser := newMockParser(ctrl)
+ if len(tc.platform) == 1 {
+ tc.mock(mockParser)
+ }
+ r := isBuildPlatformDefault(tc.platform, mockParser)
+ assert.Equal(t, r, tc.want, tc.name)
+ })
+ }
+}
+
+func TestParseBuildctlArgsForOCILayout(t *testing.T) {
+ tests := []struct {
+ name string
+ ociLayoutName string
+ ociLayoutPath string
+ expectedArgs []string
+ errorIsNil bool
+ expectedErr string
+ }{
+ {
+ name: "PrefixNotFoundError",
+ ociLayoutName: "unit-test",
+ ociLayoutPath: "/tmp/oci-layout/",
+ expectedArgs: []string{},
+ expectedErr: ErrOCILayoutPrefixNotFound.Error(),
+ },
+ {
+ name: "DirectoryNotFoundError",
+ ociLayoutName: "unit-test",
+ ociLayoutPath: "oci-layout:///tmp/oci-layout",
+ expectedArgs: []string{},
+ expectedErr: "open /tmp/oci-layout/index.json: no such file or directory",
+ },
+ }
+
+ if runtime.GOOS == "windows" {
+ abspath, err := filepath.Abs("/tmp/oci-layout")
+ assert.NilError(t, err)
+ tests[1].expectedErr = fmt.Sprintf(
+ "open %s\\index.json: The system cannot find the path specified.",
+ abspath,
+ )
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ args, err := parseBuildContextFromOCILayout(test.ociLayoutName, test.ociLayoutPath)
+ if test.errorIsNil {
+ assert.NilError(t, err)
+ } else {
+ assert.Error(t, err, test.expectedErr)
+ }
+ assert.Equal(t, len(args), len(test.expectedArgs))
+ assert.DeepEqual(t, args, test.expectedArgs)
+ })
+ }
+}
diff --git a/pkg/cmd/builder/prune.go b/pkg/cmd/builder/prune.go
index 18ba6dc0b49..541507f6cd0 100644
--- a/pkg/cmd/builder/prune.go
+++ b/pkg/cmd/builder/prune.go
@@ -23,9 +23,10 @@ import (
"io"
"os/exec"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/buildkitutil"
)
// Prune will prune all build cache.
@@ -40,7 +41,7 @@ func Prune(ctx context.Context, options types.BuilderPruneOptions) ([]buildkitut
buildctlArgs = append(buildctlArgs, "--all")
}
buildctlCmd := exec.Command(buildctlBinary, buildctlArgs...)
- logrus.Debugf("running %v", buildctlCmd.Args)
+ log.G(ctx).Debugf("running %v", buildctlCmd.Args)
buildctlCmd.Stderr = options.Stderr
stdout, err := buildctlCmd.StdoutPipe()
if err != nil {
diff --git a/pkg/cmd/compose/compose.go b/pkg/cmd/compose/compose.go
index 1b5cd8d37c0..ba6e0868af1 100644
--- a/pkg/cmd/compose/compose.go
+++ b/pkg/cmd/compose/compose.go
@@ -23,25 +23,34 @@ import (
"os"
"path/filepath"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
- "github.com/containerd/nerdctl/pkg/composer"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/ipfs"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/containerd/nerdctl/pkg/signutil"
- "github.com/containerd/nerdctl/pkg/strutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
+ "github.com/containerd/nerdctl/v2/pkg/composer"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/ipfs"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+ "github.com/containerd/nerdctl/v2/pkg/signutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
+//nolint:unused
+var locked *os.File
+
// New returns a new *composer.Composer.
func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, options composer.Options, stdout, stderr io.Writer) (*composer.Composer, error) {
- cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithDefaultNetwork())
+ if err := composer.Lock(globalOptions.DataRoot, globalOptions.Address); err != nil {
+ return nil, err
+ }
+
+ cniEnv, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(globalOptions.Namespace), netutil.WithDefaultNetwork(globalOptions.BridgeIP))
if err != nil {
return nil, err
}
@@ -75,22 +84,15 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op
if err != nil {
return nil, err
}
- options.VolumeExists = func(volName string) (bool, error) {
- if _, volGetErr := volStore.Get(volName, false); volGetErr == nil {
- return true, nil
- } else if errors.Is(volGetErr, errdefs.ErrNotFound) {
- return false, nil
- } else {
- return false, volGetErr
- }
- }
+ // FIXME: this is racy. See note in up_volume.go
+ options.VolumeExists = volStore.Exists
options.ImageExists = func(ctx context.Context, rawRef string) (bool, error) {
- refNamed, err := referenceutil.ParseAny(rawRef)
+ parsedReference, err := referenceutil.Parse(rawRef)
if err != nil {
return false, err
}
- ref := refNamed.String()
+ ref := parsedReference.String()
if _, err := client.ImageService().Get(ctx, ref); err != nil {
if errors.Is(err, errdefs.ErrNotFound) {
return false, nil
@@ -110,8 +112,23 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op
ocispecPlatforms = []ocispec.Platform{parsed} // no append
}
- // IPFS reference
- if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(imageName); err == nil {
+ imgPullOpts := types.ImagePullOptions{
+ GOptions: globalOptions,
+ OCISpecPlatform: ocispecPlatforms,
+ Unpack: nil,
+ Mode: pullMode,
+ Quiet: quiet,
+ RFlags: types.RemoteSnapshotterFlags{},
+ Stdout: stdout,
+ Stderr: stderr,
+ }
+
+ parsedReference, err := referenceutil.Parse(imageName)
+ if err != nil {
+ return err
+ }
+
+ if parsedReference.Protocol != "" {
var ipfsPath string
if ipfsAddress := options.IPFSAddress; ipfsAddress != "" {
dir, err := os.MkdirTemp("", "apidirtmp")
@@ -124,8 +141,7 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op
}
ipfsPath = dir
}
- _, err = ipfs.EnsureImage(ctx, client, stdout, stderr, globalOptions.Snapshotter, scheme, ref,
- pullMode, ocispecPlatforms, nil, quiet, ipfsPath)
+ _, err = ipfs.EnsureImage(ctx, client, string(parsedReference.Protocol), parsedReference.String(), ipfsPath, imgPullOpts)
return err
}
@@ -135,8 +151,7 @@ func New(client *containerd.Client, globalOptions types.GlobalCommandOptions, op
return err
}
- _, err = imgutil.EnsureImage(ctx, client, stdout, stderr, globalOptions.Snapshotter, ref,
- pullMode, globalOptions.InsecureRegistry, globalOptions.HostsDir, ocispecPlatforms, nil, quiet)
+ _, err = imgutil.EnsureImage(ctx, client, ref, imgPullOpts)
return err
}
diff --git a/pkg/cmd/container/attach.go b/pkg/cmd/container/attach.go
new file mode 100644
index 00000000000..177a9fd03db
--- /dev/null
+++ b/pkg/cmd/container/attach.go
@@ -0,0 +1,167 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package container
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/containerd/console"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/signalutil"
+)
+
+// Attach attaches stdin, stdout, and stderr to a running container.
+func Attach(ctx context.Context, client *containerd.Client, req string, options types.ContainerAttachOptions) error {
+ // Find the container.
+ var container containerd.Container
+ var cStatus containerd.Status
+
+ walker := &containerwalker.ContainerWalker{
+ Client: client,
+ OnFound: func(ctx context.Context, found containerwalker.Found) error {
+ container = found.Container
+ return nil
+ },
+ }
+ n, err := walker.Walk(ctx, req)
+ if err != nil {
+ return fmt.Errorf("error when trying to find the container: %w", err)
+ }
+ if n == 0 {
+ return fmt.Errorf("no container is found given the string: %s", req)
+ } else if n > 1 {
+ return fmt.Errorf("more than one containers are found given the string: %s", req)
+ }
+
+ defer func() {
+ containerLabels, err := container.Labels(ctx)
+ if err != nil {
+ log.G(ctx).WithError(err).Errorf("failed to getting container labels: %s", err)
+ return
+ }
+ rm, err := containerutil.DecodeContainerRmOptLabel(containerLabels[labels.ContainerAutoRemove])
+ if err != nil {
+ log.G(ctx).WithError(err).Errorf("failed to decode string to bool value: %s", err)
+ return
+ }
+ if rm && cStatus.Status == containerd.Stopped {
+ if err = RemoveContainer(ctx, container, options.GOptions, true, true, client); err != nil {
+ log.L.WithError(err).Warnf("failed to remove container %s: %s", req, err)
+ }
+ }
+ }()
+
+ // Attach to the container.
+ var task containerd.Task
+ detachC := make(chan struct{})
+ spec, err := container.Spec(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get the OCI runtime spec for the container: %w", err)
+ }
+ var (
+ opt cio.Opt
+ con console.Console
+ )
+ if spec.Process.Terminal {
+ con, err = consoleutil.Current()
+ if err != nil {
+ return err
+ }
+ defer con.Reset()
+ if err := con.SetRaw(); err != nil {
+ return fmt.Errorf("failed to set the console to raw mode: %w", err)
+ }
+ closer := func() {
+ detachC <- struct{}{}
+ // task will be set by container.Task later.
+ //
+ // We cannot use container.Task(ctx, cio.Load) to get the IO here
+ // because the `cancel` field of the returned `*cio` is nil. [1]
+ //
+ // [1] https://github.com/containerd/containerd/blob/8f756bc8c26465bd93e78d9cd42082b66f276e10/cio/io.go#L358-L359
+ io := task.IO()
+ if io == nil {
+ log.G(ctx).Errorf("got a nil io")
+ return
+ }
+ io.Cancel()
+ }
+ in, err := consoleutil.NewDetachableStdin(con, options.DetachKeys, closer)
+ if err != nil {
+ return err
+ }
+ opt = cio.WithStreams(in, con, nil)
+ } else {
+ opt = cio.WithStreams(options.Stdin, options.Stdout, options.Stderr)
+ }
+ task, err = container.Task(ctx, cio.NewAttach(opt))
+ if err != nil {
+ return fmt.Errorf("failed to attach to the container: %w", err)
+ }
+ if spec.Process.Terminal {
+ if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil {
+ log.G(ctx).WithError(err).Error("console resize")
+ }
+ }
+ sigC := signalutil.ForwardAllSignals(ctx, task)
+ defer signalutil.StopCatch(sigC)
+
+ // Wait for the container to exit.
+ statusC, err := task.Wait(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to init an async wait for the container to exit: %w", err)
+ }
+ select {
+ // io.Wait() would return when either 1) the user detaches from the container OR 2) the container is about to exit.
+ //
+ // If we replace the `select` block with io.Wait() and
+ // directly use task.Status() to check the status of the container after io.Wait() returns,
+ // it can still be running even though the container is about to exit (somehow especially for Windows).
+ //
+ // As a result, we need a separate detachC to distinguish from the 2 cases mentioned above.
+ case <-detachC:
+ io := task.IO()
+ if io == nil {
+ return errors.New("got a nil IO from the task")
+ }
+ io.Wait()
+ case status := <-statusC:
+ cStatus, err = task.Status(ctx)
+ if err != nil {
+ return err
+ }
+ code, _, err := status.Result()
+ if err != nil {
+ return err
+ }
+ if code != 0 {
+ return errutil.NewExitCoderErr(int(code))
+ }
+ }
+ return nil
+}
diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go
index 2b9a556fcd7..1e089c7e92c 100644
--- a/pkg/cmd/container/commit.go
+++ b/pkg/cmd/container/commit.go
@@ -22,17 +22,18 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/imgutil/commit"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/commit"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
// Commit will commit a container’s file changes or settings into a new image.
func Commit(ctx context.Context, client *containerd.Client, rawRef string, req string, options types.ContainerCommitOptions) error {
- named, err := referenceutil.ParseDockerRef(rawRef)
+ parsedReference, err := referenceutil.Parse(rawRef)
if err != nil {
return err
}
@@ -45,7 +46,7 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s
opts := &commit.Opts{
Author: options.Author,
Message: options.Message,
- Ref: named.String(),
+ Ref: parsedReference.String(),
Pause: options.Pause,
Changes: changes,
}
@@ -56,11 +57,11 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s
if found.MatchCount > 1 {
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
}
- imageID, err := commit.Commit(ctx, client, found.Container, opts)
+ imageID, err := commit.Commit(ctx, client, found.Container, opts, options.GOptions)
if err != nil {
return err
}
- _, err = fmt.Fprintf(options.Stdout, "%s\n", imageID)
+ _, err = fmt.Fprintln(options.Stdout, imageID)
return err
},
}
@@ -96,7 +97,7 @@ func parseChanges(userChanges []string) (commit.Changes, error) {
return commit.Changes{}, fmt.Errorf("malformed json in change flag value %q", change)
}
if changes.CMD != nil {
- logrus.Warn("multiple change flags supplied for the CMD directive, overriding with last supplied")
+ log.L.Warn("multiple change flags supplied for the CMD directive, overriding with last supplied")
}
changes.CMD = overrideCMD
case entrypointDirective:
@@ -105,7 +106,7 @@ func parseChanges(userChanges []string) (commit.Changes, error) {
return commit.Changes{}, fmt.Errorf("malformed json in change flag value %q", change)
}
if changes.Entrypoint != nil {
- logrus.Warnf("multiple change flags supplied for the Entrypoint directive, overriding with last supplied")
+ log.L.Warnf("multiple change flags supplied for the Entrypoint directive, overriding with last supplied")
}
changes.Entrypoint = overrideEntrypoint
default: // TODO: Support the rest of the change directives
diff --git a/pkg/cmd/container/cp_linux.go b/pkg/cmd/container/cp_linux.go
index c1d7dcca8ec..0763a376793 100644
--- a/pkg/cmd/container/cp_linux.go
+++ b/pkg/cmd/container/cp_linux.go
@@ -18,18 +18,44 @@ package container
import (
"context"
+ "fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Cp copies files/folders between a running container and the local filesystem.
-func Cp(ctx context.Context, options types.ContainerCpOptions) error {
- return containerutil.CopyFiles(
- ctx,
- options.Container2Host,
- options.Pid,
- options.DestPath,
- options.SrcPath,
- options.FollowSymLink)
+func Cp(ctx context.Context, client *containerd.Client, options types.ContainerCpOptions) error {
+ walker := &containerwalker.ContainerWalker{
+ Client: client,
+ OnFound: func(ctx context.Context, found containerwalker.Found) error {
+ if found.MatchCount > 1 {
+ return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ return containerutil.CopyFiles(
+ ctx,
+ client,
+ found.Container,
+ options)
+ },
+ }
+ count, err := walker.Walk(ctx, options.ContainerReq)
+
+ if count == -1 {
+ if err == nil {
+ panic("nil error and count == -1 from ContainerWalker.Walk should never happen")
+ }
+ err = fmt.Errorf("unable to copy: %w", err)
+ } else if count == 0 {
+ if err != nil {
+ err = fmt.Errorf("unable to retrieve containers with error: %w", err)
+ } else {
+ err = fmt.Errorf("no container found for: %s", options.ContainerReq)
+ }
+ }
+
+ return err
}
diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go
index 558b798ac4b..72be3454299 100644
--- a/pkg/cmd/container/create.go
+++ b/pkg/cmd/container/create.go
@@ -24,40 +24,60 @@ import (
"net/url"
"os"
"os/exec"
- "path"
"path/filepath"
"runtime"
"strconv"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- gocni "github.com/containerd/go-cni"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/image"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/flagutil"
- "github.com/containerd/nerdctl/pkg/idgen"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/logging"
- "github.com/containerd/nerdctl/pkg/mountutil"
- "github.com/containerd/nerdctl/pkg/namestore"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/containerd/nerdctl/pkg/strutil"
dockercliopts "github.com/docker/cli/opts"
- dockeropts "github.com/docker/docker/opts"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/go-cni"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/annotations"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
+ "github.com/containerd/nerdctl/v2/pkg/flagutil"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/load"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/ipcutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/logging"
+ "github.com/containerd/nerdctl/v2/pkg/maputil"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil"
+ "github.com/containerd/nerdctl/v2/pkg/namestore"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/store"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// Create will create a container.
func Create(ctx context.Context, client *containerd.Client, args []string, netManager containerutil.NetworkOptionsManager, options types.ContainerCreateOptions) (containerd.Container, func(), error) {
+ // Acquire an exclusive lock on the volume store until we are done to avoid being raced by any other
+ // volume operations (or any other operation involving volume manipulation)
+ volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
+ if err != nil {
+ return nil, nil, err
+ }
+ err = volStore.Lock()
+ if err != nil {
+ return nil, nil, err
+ }
+ defer volStore.Release()
+
// simulate the behavior of double dash
newArg := []string{}
if len(args) >= 2 && args[1] == "--" {
@@ -99,10 +119,43 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
platformOpts, err := setPlatformOptions(ctx, client, id, netManager.NetworkOptions().UTSNamespace, &internalLabels, options)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
opts = append(opts, platformOpts...)
+ if _, err := referenceutil.Parse(args[0]); errors.Is(err, referenceutil.ErrLoadOCIArchiveRequired) {
+ imageRef := args[0]
+
+ // Load and create the platform specified by the user.
+ // If none specified, fallback to the default platform.
+ platform := []string{}
+ if options.Platform != "" {
+ platform = append(platform, options.Platform)
+ }
+
+ images, err := load.FromOCIArchive(ctx, client, imageRef, types.ImageLoadOptions{
+ Stdout: options.Stdout,
+ GOptions: options.GOptions,
+ Platform: platform,
+ AllPlatforms: false,
+ Quiet: options.ImagePullOpt.Quiet,
+ })
+ if err != nil {
+ return nil, nil, err
+ } else if len(images) == 0 {
+ // This is a regression and should not occur.
+ return nil, nil, errors.New("OCI archive did not contain any images")
+ }
+
+ image := images[0].Name
+ // Multiple images loaded from the provided archive. Default to the first image found.
+ if len(images) != 1 {
+ log.L.Warnf("multiple images are found for the platform, defaulting to image %s...", image)
+ }
+
+ args[0] = image
+ }
+
var ensuredImage *imgutil.EnsuredImage
if !options.Rootfs {
var platformSS []string // len: 0 or 1
@@ -111,19 +164,23 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
}
ocispecPlatforms, err := platformutil.NewOCISpecPlatformSlice(false, platformSS)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
rawRef := args[0]
- ensuredImage, err = image.EnsureImage(ctx, client, rawRef, ocispecPlatforms, options.Pull, nil, false, options.ImagePullOpt)
+ options.ImagePullOpt.Mode = options.Pull
+ options.ImagePullOpt.OCISpecPlatform = ocispecPlatforms
+ options.ImagePullOpt.Unpack = nil
+
+ ensuredImage, err = image.EnsureImage(ctx, client, rawRef, options.ImagePullOpt)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
}
rootfsOpts, rootfsCOpts, err := generateRootfsOpts(args, id, ensuredImage, options)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
opts = append(opts, rootfsOpts...)
cOpts = append(cOpts, rootfsCOpts...)
@@ -134,13 +191,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
envs, err := flagutil.MergeEnvFileAndOSEnv(options.EnvFile, options.Env)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
- opts = append(opts, oci.WithEnv(envs))
if options.Interactive {
if options.Detach {
- return nil, nil, errors.New("currently flag -i and -d cannot be specified together (FIXME)")
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), errors.New("currently flag -i and -d cannot be specified together (FIXME)")
}
}
@@ -149,9 +205,9 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
}
var mountOpts []oci.SpecOpts
- mountOpts, internalLabels.anonVolumes, internalLabels.mountPoints, err = generateMountOpts(ctx, client, ensuredImage, options)
+ mountOpts, internalLabels.anonVolumes, internalLabels.mountPoints, err = generateMountOpts(ctx, client, ensuredImage, volStore, options)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
opts = append(opts, mountOpts...)
@@ -161,40 +217,38 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
// 1, nerdctl run --name demo -it imagename
// 2, ctrl + c to stop demo container
// 3, nerdctl start/restart demo
- logConfig, err := generateLogConfig(dataStore, id, options.LogDriver, options.LogOpt, options.GOptions.Namespace)
+ logConfig, err := generateLogConfig(dataStore, id, options.LogDriver, options.LogOpt, options.GOptions.Namespace, options.GOptions.Address)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
internalLabels.logURI = logConfig.LogURI
restartOpts, err := generateRestartOpts(ctx, client, options.Restart, logConfig.LogURI, options.InRun)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err
}
cOpts = append(cOpts, restartOpts...)
- cOpts = append(cOpts, withStop(options.StopSignal, options.StopTimeout, ensuredImage))
if err = netManager.VerifyNetworkOptions(ctx); err != nil {
- return nil, nil, fmt.Errorf("failed to verify networking settings: %s", err)
+ return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), fmt.Errorf("failed to verify networking settings: %s", err)
}
netOpts, netNewContainerOpts, err := netManager.ContainerNetworkingOpts(ctx, id)
if err != nil {
- return nil, nil, fmt.Errorf("failed to generate networking spec options: %s", err)
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), fmt.Errorf("failed to generate networking spec options: %s", err)
}
opts = append(opts, netOpts...)
cOpts = append(cOpts, netNewContainerOpts...)
netLabelOpts, err := netManager.InternalNetworkingOptionLabels(ctx)
if err != nil {
- return nil, nil, fmt.Errorf("failed to generate internal networking labels: %s", err)
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), fmt.Errorf("failed to generate internal networking labels: %s", err)
}
- // TODO(aznashwan): more formal way to load net opts into internalLabels:
- internalLabels.hostname = netLabelOpts.Hostname
- internalLabels.ports = netLabelOpts.PortMappings
- internalLabels.ipAddress = netLabelOpts.IPAddress
- internalLabels.networks = netLabelOpts.NetworkSlice
- internalLabels.macAddress = netLabelOpts.MACAddress
+
+ envs = append(envs, "HOSTNAME="+netLabelOpts.Hostname)
+ opts = append(opts, oci.WithEnv(envs))
+
+ internalLabels.loadNetOpts(netLabelOpts)
// NOTE: OCI hooks are currently not supported on Windows so we skip setting them altogether.
// The OCI hooks we define (whose logic can be found in pkg/ocihook) primarily
@@ -203,37 +257,37 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
if runtime.GOOS != "windows" {
hookOpt, err := withNerdctlOCIHook(options.NerdctlCmd, options.NerdctlArgs)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
opts = append(opts, hookOpt)
}
uOpts, err := generateUserOpts(options.User)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
opts = append(opts, uOpts...)
gOpts, err := generateGroupsOpts(options.GroupAdd)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
opts = append(opts, gOpts...)
umaskOpts, err := generateUmaskOpts(options.Umask)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
opts = append(opts, umaskOpts...)
rtCOpts, err := generateRuntimeCOpts(options.GOptions.CgroupManager, options.Runtime)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
cOpts = append(cOpts, rtCOpts...)
- lCOpts, err := withContainerLabels(options.Label, options.LabelFile)
+ lCOpts, err := withContainerLabels(options.Label, options.LabelFile, ensuredImage)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
cOpts = append(cOpts, lCOpts...)
@@ -244,43 +298,42 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
if ensuredImage != nil {
imageRef = ensuredImage.Ref
}
- options.Name = referenceutil.SuggestContainerName(imageRef, id)
+ parsedReference, err := referenceutil.Parse(imageRef)
+ // Ignore cases where the imageRef is ""
+ if err != nil && imageRef != "" {
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
+ }
+ options.Name = parsedReference.SuggestContainerName(id)
}
if options.Name != "" {
containerNameStore, err = namestore.New(dataStore, options.GOptions.Namespace)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
if err := containerNameStore.Acquire(options.Name, id); err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
}
internalLabels.name = options.Name
internalLabels.pidFile = options.PidFile
- internalLabels.extraHosts = strutil.DedupeStrSlice(netManager.NetworkOptions().AddHost)
- for i, host := range internalLabels.extraHosts {
- if _, err := dockercliopts.ValidateExtraHost(host); err != nil {
- return nil, nil, err
- }
- parts := strings.SplitN(host, ":", 2)
- // If the IP Address is a string called "host-gateway", replace this value with the IP address stored
- // in the daemon level HostGateway IP config variable.
- if parts[1] == dockeropts.HostGatewayName {
- if options.GOptions.HostGatewayIP == "" {
- return nil, nil, fmt.Errorf("unable to derive the IP value for host-gateway")
- }
- parts[1] = options.GOptions.HostGatewayIP
- internalLabels.extraHosts[i] = fmt.Sprintf(`%s:%s`, parts[0], parts[1])
- }
+
+ extraHosts, err := containerutil.ParseExtraHosts(netManager.NetworkOptions().AddHost, options.GOptions.HostGatewayIP, ":")
+ if err != nil {
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
+ internalLabels.extraHosts = extraHosts
+
+ internalLabels.rm = containerutil.EncodeContainerRmOptLabel(options.Rm)
+ // TODO: abolish internal labels and only use annotations
ilOpt, err := withInternalLabels(internalLabels)
if err != nil {
- return nil, nil, err
+ return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err
}
cOpts = append(cOpts, ilOpt)
- opts = append(opts, propagateContainerdLabelsToOCIAnnotations())
+ opts = append(opts, propagateInternalContainerdLabelsToOCIAnnotations(),
+ oci.WithAnnotations(strutil.ConvertKVStringsToMap(options.Annotations)))
var s specs.Spec
spec := containerd.WithSpec(&s, opts...)
@@ -289,12 +342,11 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa
c, containerErr := client.NewContainer(ctx, id, cOpts...)
var netSetupErr error
- // NOTE: on non-Windows platforms, network setup is performed by OCI hooks.
- // Seeing as though Windows does not currently support OCI hooks, we must explicitly
- // perform network setup/teardown in the main nerdctl executable.
- if containerErr == nil && runtime.GOOS == "windows" {
+ if containerErr == nil {
netSetupErr = netManager.SetupNetworking(ctx, id)
- logrus.WithError(netSetupErr).Warnf("networking setup error has occurred")
+ if netSetupErr != nil {
+ log.G(ctx).WithError(netSetupErr).Warnf("networking setup error has occurred")
+ }
}
if containerErr != nil || netSetupErr != nil {
@@ -323,10 +375,9 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
for ind, env := range ensured.ImageConfig.Env {
if strings.HasPrefix(env, "PATH=") {
break
- } else {
- if ind == len(ensured.ImageConfig.Env)-1 {
- opts = append(opts, oci.WithDefaultPathEnv)
- }
+ }
+ if ind == len(ensured.ImageConfig.Env)-1 {
+ opts = append(opts, oci.WithDefaultPathEnv)
}
}
} else {
@@ -337,6 +388,15 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
opts = append(opts, oci.WithRootFSPath(absRootfs), oci.WithDefaultPathEnv)
}
+ entrypointPath := ""
+ if ensured != nil {
+ if len(ensured.ImageConfig.Entrypoint) > 0 {
+ entrypointPath = ensured.ImageConfig.Entrypoint[0]
+ } else if len(ensured.ImageConfig.Cmd) > 0 {
+ entrypointPath = ensured.ImageConfig.Cmd[0]
+ }
+ }
+
if !options.Rootfs && !options.EntrypointChanged {
opts = append(opts, oci.WithImageConfigArgs(ensured.Image, args[1:]))
} else {
@@ -354,8 +414,47 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
// error message is from Podman
return nil, nil, errors.New("no command or entrypoint provided, and no CMD or ENTRYPOINT from image")
}
+
+ entrypointPath = processArgs[0]
+
opts = append(opts, oci.WithProcessArgs(processArgs...))
}
+
+ isEntryPointSystemd := (entrypointPath == "/sbin/init" ||
+ entrypointPath == "/usr/sbin/init" ||
+ entrypointPath == "/usr/local/sbin/init")
+
+ stopSignal := options.StopSignal
+
+ if options.Systemd == "always" || (options.Systemd == "true" && isEntryPointSystemd) {
+ if options.Privileged {
+ securityOptsMap := strutil.ConvertKVStringsToMap(strutil.DedupeStrSlice(options.SecurityOpt))
+ privilegedWithoutHostDevices, err := maputil.MapBoolValueAsOpt(securityOptsMap, "privileged-without-host-devices")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // See: https://github.com/containers/podman/issues/15878
+ if !privilegedWithoutHostDevices {
+ return nil, nil, errors.New("if --privileged is used with systemd `--security-opt privileged-without-host-devices` must also be used")
+ }
+ }
+
+ opts = append(opts,
+ oci.WithoutMounts("/sys/fs/cgroup"),
+ oci.WithMounts([]specs.Mount{
+ {Type: "cgroup", Source: "cgroup", Destination: "/sys/fs/cgroup", Options: []string{"rw"}},
+ {Type: "tmpfs", Source: "tmpfs", Destination: "/run"},
+ {Type: "tmpfs", Source: "tmpfs", Destination: "/run/lock"},
+ {Type: "tmpfs", Source: "tmpfs", Destination: "/tmp"},
+ {Type: "tmpfs", Source: "tmpfs", Destination: "/var/lib/journal"},
+ }),
+ )
+ stopSignal = "SIGRTMIN+3"
+ }
+
+ cOpts = append(cOpts, withStop(stopSignal, options.StopTimeout, ensured))
+
if options.InitBinary != nil {
options.InitProcessFlag = true
}
@@ -385,23 +484,6 @@ func generateRootfsOpts(args []string, id string, ensured *imgutil.EnsuredImage,
return opts, cOpts, nil
}
-// withBindMountHostIPC replaces /dev/shm and /dev/mqueue mount with rbind.
-// Required for --ipc=host on rootless.
-func withBindMountHostIPC(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
- for i, m := range s.Mounts {
- switch p := path.Clean(m.Destination); p {
- case "/dev/shm", "/dev/mqueue":
- s.Mounts[i] = specs.Mount{
- Destination: p,
- Type: "bind",
- Source: p,
- Options: []string{"rbind", "nosuid", "noexec", "nodev"},
- }
- }
- }
- return nil
-}
-
// GenerateLogURI generates a log URI for the current container store
func GenerateLogURI(dataStore string) (*url.URL, error) {
selfExe, err := os.Executable()
@@ -416,6 +498,29 @@ func GenerateLogURI(dataStore string) (*url.URL, error) {
}
func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) {
+ if rootlessutil.IsRootless() {
+ detachedNetNS, err := rootlessutil.DetachedNetNS()
+ if err != nil {
+ return nil, fmt.Errorf("failed to check whether RootlessKit is running with --detach-netns: %w", err)
+ }
+ if detachedNetNS != "" {
+ // Rewrite {cmd, args} if RootlessKit is running with --detach-netns, so that the hook can gain
+ // CAP_NET_ADMIN in the namespaces.
+ // - Old:
+ // - cmd: "/usr/local/bin/nerdctl"
+ // - args: {"--data-root=/foo", "internal", "oci-hook"}
+ // - New:
+ // - cmd: "/usr/bin/nsenter"
+ // - args: {"-n/run/user/1000/containerd-rootless/netns", "-F", "--", "/usr/local/bin/nerdctl", "--data-root=/foo", "internal", "oci-hook"}
+ oldCmd, oldArgs := cmd, args
+ cmd, err = exec.LookPath("nsenter")
+ if err != nil {
+ return nil, err
+ }
+ args = append([]string{"-n" + detachedNetNS, "-F", "--", oldCmd}, oldArgs...)
+ }
+ }
+
args = append([]string{cmd}, append(args, "internal", "oci-hook")...)
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error {
if s.Hooks == nil {
@@ -438,13 +543,30 @@ func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) {
}, nil
}
-func withContainerLabels(label, labelFile []string) ([]containerd.NewContainerOpts, error) {
+func withContainerLabels(label, labelFile []string, ensuredImage *imgutil.EnsuredImage) ([]containerd.NewContainerOpts, error) {
+ var opts []containerd.NewContainerOpts
+
+ // add labels defined by image
+ if ensuredImage != nil {
+ imageLabelOpts := containerd.WithAdditionalContainerLabels(ensuredImage.ImageConfig.Labels)
+ opts = append(opts, imageLabelOpts)
+ }
+
labelMap, err := readKVStringsMapfFromLabel(label, labelFile)
if err != nil {
return nil, err
}
+ for k := range labelMap {
+ if strings.HasPrefix(k, annotations.Bypass4netns) {
+ log.L.Warnf("Label %q is deprecated, use an annotation instead", k)
+ } else if strings.HasPrefix(k, labels.Prefix) {
+ return nil, fmt.Errorf("internal label %q must not be specified manually", k)
+ }
+ }
o := containerd.WithAdditionalContainerLabels(labelMap)
- return []containerd.NewContainerOpts{o}, nil
+ opts = append(opts, o)
+
+ return opts, nil
}
func readKVStringsMapfFromLabel(label, labelFile []string) (map[string]string, error) {
@@ -486,7 +608,7 @@ func withStop(stopSignal string, stopTimeout int, ensuredImage *imgutil.EnsuredI
}
c.Labels[containerd.StopSignalLabel] = stopSignal
if stopTimeout != 0 {
- c.Labels[labels.StopTimout] = strconv.Itoa(stopTimeout)
+ c.Labels[labels.StopTimeout] = strconv.Itoa(stopTimeout)
}
return nil
}
@@ -506,15 +628,20 @@ type internalLabels struct {
// network
networks []string
ipAddress string
- ports []gocni.PortMapping
+ ip6Address string
+ ports []cni.PortMapping
macAddress string
// volume
mountPoints []*mountutil.Processed
anonVolumes []string
// pid namespace
pidContainer string
+ // ipc namespace & dev/shm
+ ipc string
// log
logURI string
+ // a label to check whether the --rm option is specified.
+ rm string
}
// WithInternalLabels sets the internal labels for a container.
@@ -562,6 +689,10 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO
m[labels.IPAddress] = internalLabels.ipAddress
}
+ if internalLabels.ip6Address != "" {
+ m[labels.IP6Address] = internalLabels.ip6Address
+ }
+
m[labels.Platform], err = platformutil.NormalizeString(internalLabels.platform)
if err != nil {
return nil, err
@@ -584,9 +715,27 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO
m[labels.PIDContainer] = internalLabels.pidContainer
}
+ if internalLabels.ipc != "" {
+ m[labels.IPC] = internalLabels.ipc
+ }
+
+ if internalLabels.rm != "" {
+ m[labels.ContainerAutoRemove] = internalLabels.rm
+ }
+
return containerd.WithAdditionalContainerLabels(m), nil
}
+// loadNetOpts loads network options into InternalLabels.
+func (il *internalLabels) loadNetOpts(opts types.NetworkOptions) {
+ il.hostname = opts.Hostname
+ il.ports = opts.PortMappings
+ il.ipAddress = opts.IPAddress
+ il.ip6Address = opts.IP6Address
+ il.networks = opts.NetworkSlice
+ il.macAddress = opts.MACAddress
+}
+
func dockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.MountPoint {
result := make([]dockercompat.MountPoint, len(mountPoints))
for i := range mountPoints {
@@ -599,6 +748,7 @@ func dockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.Mount
Driver: "",
Mode: mp.Mode,
}
+ result[i].RW, result[i].Propagation = dockercompat.ParseMountProperties(strings.Split(mp.Mode, ","))
// it's an anonymous volume
if mp.AnonymousVolume != "" {
@@ -613,14 +763,38 @@ func dockercompatMounts(mountPoints []*mountutil.Processed) []dockercompat.Mount
return result
}
-func propagateContainerdLabelsToOCIAnnotations() oci.SpecOpts {
+func processeds(mountPoints []dockercompat.MountPoint) []*mountutil.Processed {
+ result := make([]*mountutil.Processed, len(mountPoints))
+ for i := range mountPoints {
+ mp := mountPoints[i]
+ result[i] = &mountutil.Processed{
+ Type: mp.Type,
+ Name: mp.Name,
+ Mount: specs.Mount{
+ Source: mp.Source,
+ Destination: mp.Destination,
+ },
+ Mode: mp.Mode,
+ }
+ }
+ return result
+}
+
+func propagateInternalContainerdLabelsToOCIAnnotations() oci.SpecOpts {
return func(ctx context.Context, oc oci.Client, c *containers.Container, s *oci.Spec) error {
- return oci.WithAnnotations(c.Labels)(ctx, oc, c, s)
+ allowed := make(map[string]string)
+ for k, v := range c.Labels {
+ if strings.Contains(k, labels.Prefix) {
+ allowed[k] = v
+ }
+ }
+ return oci.WithAnnotations(allowed)(ctx, oc, c, s)
}
}
func writeCIDFile(path, id string) error {
- if _, err := os.Stat(path); err == nil {
+ _, err := os.Stat(path)
+ if err == nil {
return fmt.Errorf("container ID file found, make sure the other container isn't running or delete %s", path)
} else if errors.Is(err, os.ErrNotExist) {
f, err := os.Create(path)
@@ -633,77 +807,119 @@ func writeCIDFile(path, id string) error {
return err
}
return nil
- } else {
- return err
}
+ return err
}
// generateLogConfig creates a LogConfig for the current container store
-func generateLogConfig(dataStore string, id string, logDriver string, logOpt []string, ns string) (logConfig logging.LogConfig, err error) {
+func generateLogConfig(dataStore string, id string, logDriver string, logOpt []string, ns, address string) (logConfig logging.LogConfig, err error) {
var u *url.URL
if u, err = url.Parse(logDriver); err == nil && u.Scheme != "" {
logConfig.LogURI = logDriver
} else {
logConfig.Driver = logDriver
+ logConfig.Address = address
logConfig.Opts, err = parseKVStringsMapFromLogOpt(logOpt, logDriver)
if err != nil {
- return
+ return logConfig, err
}
var (
logDriverInst logging.Driver
logConfigB []byte
lu *url.URL
)
- logDriverInst, err = logging.GetDriver(logDriver, logConfig.Opts)
+ logDriverInst, err = logging.GetDriver(logDriver, logConfig.Opts, logConfig.Address)
if err != nil {
- return
+ return logConfig, err
}
if err = logDriverInst.Init(dataStore, ns, id); err != nil {
- return
+ return logConfig, err
}
logConfigB, err = json.Marshal(logConfig)
if err != nil {
- return
+ return logConfig, err
}
logConfigFilePath := logging.LogConfigFilePath(dataStore, ns, id)
if err = os.WriteFile(logConfigFilePath, logConfigB, 0600); err != nil {
- return
+ return logConfig, err
}
lu, err = GenerateLogURI(dataStore)
if err != nil {
- return
+ return logConfig, err
}
if lu != nil {
- logrus.Debugf("generated log driver: %s", lu.String())
+ log.L.Debugf("generated log driver: %s", lu.String())
logConfig.LogURI = lu.String()
}
}
return logConfig, nil
}
+func generateRemoveStateDirFunc(ctx context.Context, id string, internalLabels internalLabels) func() {
+ return func() {
+ if rmErr := os.RemoveAll(internalLabels.stateDir); rmErr != nil {
+ log.G(ctx).WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, internalLabels.stateDir)
+ }
+ }
+}
+
+func generateRemoveOrphanedDirsFunc(ctx context.Context, id, dataStore string, internalLabels internalLabels) func() {
+ return func() {
+ if rmErr := os.RemoveAll(internalLabels.stateDir); rmErr != nil {
+ log.G(ctx).WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, internalLabels.stateDir)
+ }
+
+ hs, err := hostsstore.New(dataStore, internalLabels.namespace)
+ if err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to instantiate hostsstore for %q", internalLabels.namespace)
+ } else if err = hs.Delete(id); err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to remove an etchosts directory for container %q", id)
+ }
+ }
+}
+
func generateGcFunc(ctx context.Context, container containerd.Container, ns, id, name, dataStore string, containerErr error, containerNameStore namestore.NameStore, netManager containerutil.NetworkOptionsManager, internalLabels internalLabels) func() {
return func() {
if containerErr == nil {
netGcErr := netManager.CleanupNetworking(ctx, container)
if netGcErr != nil {
- logrus.WithError(netGcErr).Warnf("failed to revert container %q networking settings", id)
+ log.G(ctx).WithError(netGcErr).Warnf("failed to revert container %q networking settings", id)
+ }
+ } else {
+ hs, err := hostsstore.New(dataStore, internalLabels.namespace)
+ if err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to instantiate hostsstore for %q", internalLabels.namespace)
+ } else {
+ if _, err := hs.HostsPath(id); err != nil {
+ log.G(ctx).WithError(err).Warnf("an etchosts directory for container %q dosen't exist", id)
+ } else if err = hs.Delete(id); err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to remove an etchosts directory for container %q", id)
+ }
}
}
+ ipc, ipcErr := ipcutil.DecodeIPCLabel(internalLabels.ipc)
+ if ipcErr != nil {
+ log.G(ctx).WithError(ipcErr).Warnf("failed to decode ipc label for container %q", id)
+ }
+ if ipcErr := ipcutil.CleanUp(ipc); ipcErr != nil {
+ log.G(ctx).WithError(ipcErr).Warnf("failed to clean up ipc for container %q", id)
+ }
if rmErr := os.RemoveAll(internalLabels.stateDir); rmErr != nil {
- logrus.WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, internalLabels.stateDir)
+ log.G(ctx).WithError(rmErr).Warnf("failed to remove container %q state dir %q", id, internalLabels.stateDir)
}
if name != "" {
var errE error
if containerNameStore, errE = namestore.New(dataStore, ns); errE != nil {
- logrus.WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id)
+ log.G(ctx).WithError(errE).Warnf("failed to instantiate container name store during cleanup for container %q", id)
}
- if errE = containerNameStore.Release(name, id); errE != nil {
- logrus.WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id)
+ // Double-releasing may happen with containers started with --rm, so, ignore NotFound errors
+ if errE := containerNameStore.Release(name, id); errE != nil && !errors.Is(errE, store.ErrNotFound) {
+ log.G(ctx).WithError(errE).Warnf("failed to release container name store for container %q (%s)", name, id)
}
}
}
diff --git a/pkg/cmd/container/exec.go b/pkg/cmd/container/exec.go
index ac163b85a23..c00c776998b 100644
--- a/pkg/cmd/container/exec.go
+++ b/pkg/cmd/container/exec.go
@@ -22,18 +22,20 @@ import (
"io"
"os"
- "github.com/containerd/console"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/consoleutil"
- "github.com/containerd/nerdctl/pkg/flagutil"
- "github.com/containerd/nerdctl/pkg/idgen"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/signalutil"
- "github.com/containerd/nerdctl/pkg/taskutil"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/console"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
+ "github.com/containerd/nerdctl/v2/pkg/flagutil"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/signalutil"
+ "github.com/containerd/nerdctl/v2/pkg/taskutil"
)
// Exec will find the right running container to run a new command.
@@ -104,7 +106,10 @@ func execActionWithContainer(ctx context.Context, client *containerd.Client, con
var con console.Console
if options.TTY {
- con = console.Current()
+ con, err = consoleutil.Current()
+ if err != nil {
+ return err
+ }
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
@@ -113,7 +118,7 @@ func execActionWithContainer(ctx context.Context, client *containerd.Client, con
if !options.Detach {
if options.TTY {
if err := consoleutil.HandleConsoleResize(ctx, process, con); err != nil {
- logrus.WithError(err).Error("console resize")
+ log.G(ctx).WithError(err).Error("console resize")
}
} else {
sigc := signalutil.ForwardAllSignals(ctx, process)
@@ -162,7 +167,11 @@ func generateExecProcessSpec(ctx context.Context, client *containerd.Client, con
pspec := spec.Process
pspec.Terminal = options.TTY
if pspec.Terminal {
- if size, err := console.Current().Size(); err == nil {
+ con, err := consoleutil.Current()
+ if err != nil {
+ return nil, err
+ }
+ if size, err := con.Size(); err == nil {
pspec.ConsoleSize = &specs.Box{Height: uint(size.Height), Width: uint(size.Width)}
}
}
diff --git a/pkg/cmd/container/exec_linux.go b/pkg/cmd/container/exec_linux.go
index 28fb7c09028..33159b77dc9 100644
--- a/pkg/cmd/container/exec_linux.go
+++ b/pkg/cmd/container/exec_linux.go
@@ -17,8 +17,9 @@
package container
import (
- "github.com/containerd/containerd/pkg/cap"
"github.com/opencontainers/runtime-spec/specs-go"
+
+ "github.com/containerd/containerd/v2/pkg/cap"
)
func setExecCapabilities(pspec *specs.Process) error {
diff --git a/pkg/cmd/container/inspect.go b/pkg/cmd/container/inspect.go
index 07b5f3dca61..72db92d4248 100644
--- a/pkg/cmd/container/inspect.go
+++ b/pkg/cmd/container/inspect.go
@@ -21,19 +21,25 @@ import (
"fmt"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerinspector"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerdutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerinspector"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
)
// Inspect prints detailed information for each container in `containers`.
func Inspect(ctx context.Context, client *containerd.Client, containers []string, options types.ContainerInspectOptions) error {
f := &containerInspector{
- mode: options.Mode,
+ mode: options.Mode,
+ size: options.Size,
+ snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter),
}
walker := &containerwalker.ContainerWalker{
@@ -44,15 +50,18 @@ func Inspect(ctx context.Context, client *containerd.Client, containers []string
err := walker.WalkAll(ctx, containers, true)
if len(f.entries) > 0 {
if formatErr := formatter.FormatSlice(options.Format, options.Stdout, f.entries); formatErr != nil {
- logrus.Error(formatErr)
+ log.L.Error(formatErr)
}
}
+
return err
}
type containerInspector struct {
- mode string
- entries []interface{}
+ mode string
+ size bool
+ snapshotter snapshots.Snapshotter
+ entries []interface{}
}
func (x *containerInspector) Handler(ctx context.Context, found containerwalker.Found) error {
@@ -71,7 +80,15 @@ func (x *containerInspector) Handler(ctx context.Context, found containerwalker.
if err != nil {
return err
}
+ if x.size {
+ resourceUsage, allResourceUsage, err := imgutil.ResourceUsage(ctx, x.snapshotter, d.ID)
+ if err == nil {
+ d.SizeRw = &resourceUsage.Size
+ d.SizeRootFs = &allResourceUsage.Size
+ }
+ }
x.entries = append(x.entries, d)
+ return err
default:
return fmt.Errorf("unknown mode %q", x.mode)
}
diff --git a/pkg/cmd/container/kill.go b/pkg/cmd/container/kill.go
index 5652dbf9031..ceb4ef9ac88 100644
--- a/pkg/cmd/container/kill.go
+++ b/pkg/cmd/container/kill.go
@@ -18,18 +18,28 @@ package container
import (
"context"
+ "encoding/json"
"fmt"
"os"
"strings"
"syscall"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
"github.com/moby/sys/signal"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/go-cni"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/netutil/nettype"
+ "github.com/containerd/nerdctl/v2/pkg/portutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
// Kill kills a list of containers
@@ -49,6 +59,9 @@ func Kill(ctx context.Context, client *containerd.Client, reqs []string, options
if found.MatchCount > 1 {
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
}
+ if err := cleanupNetwork(ctx, found.Container, options.GOptions); err != nil {
+ return fmt.Errorf("unable to cleanup network for container: %s, %q", found.Req, err)
+ }
if err := killContainer(ctx, found.Container, parsedSignal); err != nil {
if errdefs.IsNotFound(err) {
fmt.Fprintf(options.Stderr, "No such container: %s\n", found.Req)
@@ -56,7 +69,7 @@ func Kill(ctx context.Context, client *containerd.Client, reqs []string, options
}
return err
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Container.ID())
+ _, err := fmt.Fprintln(options.Stdout, found.Container.ID())
return err
},
}
@@ -64,7 +77,15 @@ func Kill(ctx context.Context, client *containerd.Client, reqs []string, options
return walker.WalkAll(ctx, reqs, true)
}
-func killContainer(ctx context.Context, container containerd.Container, signal syscall.Signal) error {
+func killContainer(ctx context.Context, container containerd.Container, signal syscall.Signal) (err error) {
+ defer func() {
+ if err != nil {
+ containerutil.UpdateErrorLabel(ctx, container, err)
+ }
+ }()
+ if err := containerutil.UpdateExplicitlyStoppedLabel(ctx, container, true); err != nil {
+ return err
+ }
task, err := container.Task(ctx, cio.Load)
if err != nil {
return err
@@ -92,8 +113,78 @@ func killContainer(ctx context.Context, container containerd.Container, signal s
// signal will be sent once resume is finished
if paused {
if err := task.Resume(ctx); err != nil {
- logrus.Warnf("cannot unpause container %s: %s", container.ID(), err)
+ log.G(ctx).Warnf("cannot unpause container %s: %s", container.ID(), err)
}
}
return nil
}
+
+// cleanupNetwork removes cni network setup, specifically the forwards
+func cleanupNetwork(ctx context.Context, container containerd.Container, globalOpts types.GlobalCommandOptions) error {
+ return rootlessutil.WithDetachedNetNSIfAny(func() error {
+ // retrieve info to get current active port mappings
+ info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
+ if err != nil {
+ return err
+ }
+ ports, portErr := portutil.ParsePortsLabel(info.Labels)
+ if portErr != nil {
+ return fmt.Errorf("no oci spec: %q", portErr)
+ }
+ portMappings := []cni.NamespaceOpts{
+ cni.WithCapabilityPortMap(ports),
+ }
+
+ // retrieve info to get cni instance
+ spec, err := container.Spec(ctx)
+ if err != nil {
+ return err
+ }
+ networksJSON := spec.Annotations[labels.Networks]
+ var networks []string
+ if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil {
+ return err
+ }
+ netType, err := nettype.Detect(networks)
+ if err != nil {
+ return err
+ }
+
+ switch netType {
+ case nettype.Host, nettype.None, nettype.Container, nettype.Namespace:
+ // NOP
+ case nettype.CNI:
+ e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithNamespace(globalOpts.Namespace), netutil.WithDefaultNetwork(globalOpts.BridgeIP))
+ if err != nil {
+ return err
+ }
+ cniOpts := []cni.Opt{
+ cni.WithPluginDir([]string{globalOpts.CNIPath}),
+ }
+ var netw *netutil.NetworkConfig
+ for _, netstr := range networks {
+ if netw, err = e.NetworkByNameOrID(netstr); err != nil {
+ return err
+ }
+ cniOpts = append(cniOpts, cni.WithConfListBytes(netw.Bytes))
+ }
+ cniObj, err := cni.New(cniOpts...)
+ if err != nil {
+ return err
+ }
+
+ var namespaceOpts []cni.NamespaceOpts
+ namespaceOpts = append(namespaceOpts, portMappings...)
+ namespace := spec.Annotations[labels.Namespace]
+ fullID := namespace + "-" + container.ID()
+ if err := cniObj.Remove(ctx, fullID, "", namespaceOpts...); err != nil {
+ log.L.WithError(err).Errorf("failed to call cni.Remove")
+ return err
+ }
+ return nil
+ default:
+ return fmt.Errorf("unexpected network type %v", netType)
+ }
+ return nil
+ })
+}
diff --git a/pkg/cmd/container/list.go b/pkg/cmd/container/list.go
index 0db83bc60fb..b23dbb9e14b 100644
--- a/pkg/cmd/container/list.go
+++ b/pkg/cmd/container/list.go
@@ -17,36 +17,35 @@
package container
import (
- "bytes"
"context"
"encoding/json"
- "errors"
"fmt"
"sort"
"strings"
- "text/tabwriter"
- "text/template"
+ "sync"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/labels/k8slabels"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/containerd/v2/pkg/progress"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerdutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
// List prints containers according to `options`.
-func List(ctx context.Context, client *containerd.Client, options types.ContainerListOptions) error {
- containers, err := filterContainers(ctx, client, options.Filters, options.LastN, options.All)
+func List(ctx context.Context, client *containerd.Client, options types.ContainerListOptions) ([]ListItem, error) {
+ containers, cMap, err := filterContainers(ctx, client, options.Filters, options.LastN, options.All)
if err != nil {
- return err
+ return nil, err
}
- return printContainers(ctx, client, containers, options)
+ return prepareContainers(ctx, client, containers, cMap, options)
}
// filterContainers returns containers matching the filters.
@@ -55,44 +54,66 @@ func List(ctx context.Context, client *containerd.Client, options types.Containe
// - all means showing all containers (default shows just running).
// - lastN means only showing n last created containers (includes all states). Non-positive values are ignored.
// In other words, if lastN is positive, all will be set to true.
-func filterContainers(ctx context.Context, client *containerd.Client, filters []string, lastN int, all bool) ([]containerd.Container, error) {
+func filterContainers(ctx context.Context, client *containerd.Client, filters []string, lastN int, all bool) ([]containerd.Container, map[string]string, error) {
containers, err := client.Containers(ctx)
if err != nil {
- return nil, err
+ return nil, nil, err
}
filterCtx, err := foldContainerFilters(ctx, containers, filters)
if err != nil {
- return nil, err
+ return nil, nil, err
}
containers = filterCtx.MatchesFilters(ctx)
+
+ sort.Slice(containers, func(i, j int) bool {
+ infoI, _ := containers[i].Info(ctx, containerd.WithoutRefreshedMetadata)
+ infoJ, _ := containers[j].Info(ctx, containerd.WithoutRefreshedMetadata)
+ return infoI.CreatedAt.After(infoJ.CreatedAt)
+ })
+
if lastN > 0 {
all = true
- sort.Slice(containers, func(i, j int) bool {
- infoI, _ := containers[i].Info(ctx, containerd.WithoutRefreshedMetadata)
- infoJ, _ := containers[j].Info(ctx, containerd.WithoutRefreshedMetadata)
- return infoI.CreatedAt.After(infoJ.CreatedAt)
- })
if lastN < len(containers) {
containers = containers[:lastN]
}
}
- if all {
- return containers, nil
+ var wg sync.WaitGroup
+ statusPerContainer := make(map[string]string)
+ var mu sync.Mutex
+ // formatter.ContainerStatus(ctx, c) is time consuming so we do it in goroutines and return the container's id with status as a map.
+ // prepareContainers func will use this map to avoid call formatter.ContainerStatus again.
+ for _, c := range containers {
+ if c.ID() == "" {
+ return nil, nil, fmt.Errorf("container id is nill")
+ }
+ wg.Add(1)
+ go func(ctx context.Context, c containerd.Container) {
+ defer wg.Done()
+ cStatus := formatter.ContainerStatus(ctx, c)
+ mu.Lock()
+ statusPerContainer[c.ID()] = cStatus
+ mu.Unlock()
+ }(ctx, c)
+ }
+ wg.Wait()
+ if all || filterCtx.all {
+ return containers, statusPerContainer, nil
}
+
var upContainers []containerd.Container
for _, c := range containers {
- cStatus := formatter.ContainerStatus(ctx, c)
+ cStatus := statusPerContainer[c.ID()]
if strings.HasPrefix(cStatus, "Up") {
upContainers = append(upContainers, c)
}
}
- return upContainers, nil
+ return upContainers, statusPerContainer, nil
}
-type containerPrintable struct {
+type ListItem struct {
Command string
- CreatedAt string
+ CreatedAt time.Time
ID string
Image string
Platform string // nerdctl extension
@@ -102,227 +123,97 @@ type containerPrintable struct {
Runtime string // nerdctl extension
Size string
Labels string
+ LabelsMap map[string]string `json:"-"`
+
// TODO: "LocalVolumes", "Mounts", "Networks", "RunningFor", "State"
}
-func printContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, options types.ContainerListOptions) error {
- w := options.Stdout
- var (
- wide bool
- tmpl *template.Template
- )
- switch options.Format {
- case "", "table":
- w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
- if !options.Quiet {
- printHeader := "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES"
- if options.Size {
- printHeader += "\tSIZE"
- }
- fmt.Fprintln(w, printHeader)
- }
- case "raw":
- return errors.New("unsupported format: \"raw\"")
- case "wide":
- w = tabwriter.NewWriter(w, 4, 8, 4, ' ', 0)
- if !options.Quiet {
- fmt.Fprintln(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES\tRUNTIME\tPLATFORM\tSIZE")
- wide = true
- }
- default:
- if options.Quiet {
- return errors.New("format and quiet must not be specified together")
- }
- var err error
- tmpl, err = formatter.ParseTemplate(options.Format)
- if err != nil {
- return err
- }
- }
+func (x *ListItem) Label(s string) string {
+ return x.LabelsMap[s]
+}
- for _, c := range containers {
+func prepareContainers(ctx context.Context, client *containerd.Client, containers []containerd.Container, statusPerContainer map[string]string, options types.ContainerListOptions) ([]ListItem, error) {
+ listItems := make([]ListItem, len(containers))
+ snapshottersCache := map[string]snapshots.Snapshotter{}
+ for i, c := range containers {
info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata)
if err != nil {
if errdefs.IsNotFound(err) {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
continue
}
- return err
+ return nil, err
}
-
spec, err := c.Spec(ctx)
if err != nil {
if errdefs.IsNotFound(err) {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
continue
}
- return err
+ return nil, err
}
-
- imageName := info.Image
id := c.ID()
if options.Truncate && len(id) > 12 {
id = id[:12]
}
-
- p := containerPrintable{
+ var status string
+ if s, ok := statusPerContainer[c.ID()]; ok {
+ status = s
+ } else {
+ return nil, fmt.Errorf("can't get container %s status", c.ID())
+ }
+ li := ListItem{
Command: formatter.InspectContainerCommand(spec, options.Truncate, true),
- CreatedAt: info.CreatedAt.Round(time.Second).Local().String(), // format like "2021-08-07 02:19:45 +0900 JST"
+ CreatedAt: info.CreatedAt,
ID: id,
- Image: imageName,
+ Image: info.Image,
Platform: info.Labels[labels.Platform],
- Names: getPrintableContainerName(info.Labels),
+ Names: containerutil.GetContainerName(info.Labels),
Ports: formatter.FormatPorts(info.Labels),
- Status: formatter.ContainerStatus(ctx, c),
+ Status: status,
Runtime: info.Runtime.Name,
Labels: formatter.FormatLabels(info.Labels),
+ LabelsMap: info.Labels,
}
-
- if options.Size || wide {
- containerSize, err := getContainerSize(ctx, client, c, info)
- if err != nil {
- return err
- }
- p.Size = containerSize
- }
-
- if tmpl != nil {
- var b bytes.Buffer
- if err := tmpl.Execute(&b, p); err != nil {
- return err
- }
- if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil {
- return err
+ if options.Size {
+ snapshotter, ok := snapshottersCache[info.Snapshotter]
+ if !ok {
+ snapshottersCache[info.Snapshotter] = containerdutil.SnapshotService(client, info.Snapshotter)
+ snapshotter = snapshottersCache[info.Snapshotter]
}
- } else if options.Quiet {
- if _, err := fmt.Fprintf(w, "%s\n", id); err != nil {
- return err
- }
- } else {
- format := "%s\t%s\t%s\t%s\t%s\t%s\t%s"
- args := []interface{}{
- p.ID,
- p.Image,
- p.Command,
- formatter.TimeSinceInHuman(info.CreatedAt),
- p.Status,
- p.Ports,
- p.Names,
- }
- if wide {
- format += "\t%s\t%s\t%s\n"
- args = append(args, p.Runtime, p.Platform, p.Size)
- } else if options.Size {
- format += "\t%s\n"
- args = append(args, p.Size)
- } else {
- format += "\n"
- }
- if _, err := fmt.Fprintf(w, format, args...); err != nil {
- return err
- }
- }
-
- }
- if f, ok := w.(formatter.Flusher); ok {
- return f.Flush()
- }
- return nil
-}
-
-func getPrintableContainerName(containerLabels map[string]string) string {
- if name, ok := containerLabels[labels.Name]; ok {
- return name
- }
-
- if ns, ok := containerLabels[k8slabels.PodNamespace]; ok {
- if podName, ok := containerLabels[k8slabels.PodName]; ok {
- if containerName, ok := containerLabels[k8slabels.ContainerName]; ok {
- // Container
- return fmt.Sprintf("k8s://%s/%s/%s", ns, podName, containerName)
- }
- // Pod sandbox
- return fmt.Sprintf("k8s://%s/%s", ns, podName)
- }
- }
- return ""
-}
-
-type containerVolume struct {
- Type string
- Name string
- Source string
- Destination string
- Mode string
- RW bool
- Propagation string
-}
-
-func getContainerVolumes(containerLabels map[string]string) []*containerVolume {
- var vols []*containerVolume
- volLabels := []string{labels.AnonymousVolumes, labels.Mounts}
- for _, volLabel := range volLabels {
- names, ok := containerLabels[volLabel]
- if !ok {
- continue
- }
- var (
- volumes []*containerVolume
- err error
- )
- if volLabel == labels.Mounts {
- err = json.Unmarshal([]byte(names), &volumes)
- }
- if volLabel == labels.AnonymousVolumes {
- var anonymous []string
- err = json.Unmarshal([]byte(names), &anonymous)
- for _, anony := range anonymous {
- volumes = append(volumes, &containerVolume{Name: anony})
+ containerSize, err := getContainerSize(ctx, snapshotter, info.SnapshotKey)
+ if err != nil {
+ return nil, err
}
-
- }
- if err != nil {
- logrus.Warn(err)
+ li.Size = containerSize
}
- vols = append(vols, volumes...)
+ listItems[i] = li
}
- return vols
+ return listItems, nil
}
func getContainerNetworks(containerLables map[string]string) []string {
var networks []string
if names, ok := containerLables[labels.Networks]; ok {
if err := json.Unmarshal([]byte(names), &networks); err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
}
}
return networks
}
-func getContainerSize(ctx context.Context, client *containerd.Client, c containerd.Container, info containers.Container) (string, error) {
+func getContainerSize(ctx context.Context, snapshotter snapshots.Snapshotter, snapshotKey string) (string, error) {
// get container snapshot size
- snapshotKey := info.SnapshotKey
var containerSize int64
+ var imageSize int64
if snapshotKey != "" {
- usage, err := client.SnapshotService(info.Snapshotter).Usage(ctx, snapshotKey)
+ rw, all, err := imgutil.ResourceUsage(ctx, snapshotter, snapshotKey)
if err != nil {
return "", err
}
- containerSize = usage.Size
- }
-
- // get the image interface
- image, err := c.Image(ctx)
- if err != nil {
- return "", err
- }
-
- sn := client.SnapshotService(info.Snapshotter)
-
- imageSize, err := imgutil.UnpackedImageSize(ctx, sn, image)
- if err != nil {
- return "", err
+ containerSize = rw.Size
+ imageSize = all.Size
}
return fmt.Sprintf("%s (virtual %s)", progress.Bytes(containerSize).String(), progress.Bytes(imageSize).String()), nil
diff --git a/pkg/cmd/container/list_util.go b/pkg/cmd/container/list_util.go
index 9a1aac22b37..c6e0da8e2eb 100644
--- a/pkg/cmd/container/list_util.go
+++ b/pkg/cmd/container/list_util.go
@@ -23,9 +23,11 @@ import (
"strings"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
)
func foldContainerFilters(ctx context.Context, containers []containerd.Container, filters []string) (*containerFilterContext, error) {
@@ -44,8 +46,10 @@ type containerFilterContext struct {
sinceFilterFuncs []func(t time.Time) bool
statusFilterFuncs []func(containerd.ProcessStatus) bool
labelFilterFuncs []func(map[string]string) bool
- volumeFilterFuncs []func([]*containerVolume) bool
+ volumeFilterFuncs []func([]*containerutil.ContainerVolume) bool
networkFilterFuncs []func([]string) bool
+
+ all bool
}
func (cl *containerFilterContext) MatchesFilters(ctx context.Context) []containerd.Container {
@@ -120,7 +124,7 @@ func (cl *containerFilterContext) foldStatusFilter(_ context.Context, filter, va
return containerd.Stopped == stats
})
case containerd.ProcessStatus("restarting"), containerd.ProcessStatus("removing"), containerd.ProcessStatus("dead"):
- logrus.Warnf("%s is not supported and is ignored", filter)
+ log.L.Warnf("%s is not supported and is ignored", filter)
default:
return fmt.Errorf("invalid filter '%s'", filter)
}
@@ -187,7 +191,7 @@ func (cl *containerFilterContext) foldLabelFilter(_ context.Context, filter, val
}
func (cl *containerFilterContext) foldVolumeFilter(_ context.Context, filter, value string) error {
- cl.volumeFilterFuncs = append(cl.volumeFilterFuncs, func(vols []*containerVolume) bool {
+ cl.volumeFilterFuncs = append(cl.volumeFilterFuncs, func(vols []*containerutil.ContainerVolume) bool {
for _, vol := range vols {
if (vol.Source != "" && vol.Source == value) ||
(vol.Destination != "" && vol.Destination == value) ||
@@ -231,12 +235,12 @@ func (cl *containerFilterContext) matchesTaskFilters(ctx context.Context, contai
defer cancel()
task, err := container.Task(ctx, nil)
if err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
return false
}
status, err := task.Status(ctx)
if err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
return false
}
return cl.matchesExitedFilter(status) && cl.matchesStatusFilter(status)
@@ -262,6 +266,7 @@ func (cl *containerFilterContext) matchesStatusFilter(status containerd.Status)
if len(cl.statusFilterFuncs) == 0 {
return true
}
+ cl.all = true
for _, statusFilterFunc := range cl.statusFilterFuncs {
if !statusFilterFunc(status.Status) {
continue
@@ -288,7 +293,7 @@ func (cl *containerFilterContext) matchesNameFilter(info containers.Container) b
if len(cl.nameFilterFuncs) == 0 {
return true
}
- cName := getPrintableContainerName(info.Labels)
+ cName := containerutil.GetContainerName(info.Labels)
for _, nameFilterFunc := range cl.nameFilterFuncs {
if !nameFilterFunc(cName) {
continue
@@ -337,7 +342,7 @@ func (cl *containerFilterContext) matchesVolumeFilter(info containers.Container)
if len(cl.volumeFilterFuncs) == 0 {
return true
}
- vols := getContainerVolumes(info.Labels)
+ vols := containerutil.GetContainerVolumes(info.Labels)
for _, volumeFilterFunc := range cl.volumeFilterFuncs {
if !volumeFilterFunc(vols) {
continue
@@ -367,7 +372,7 @@ func idOrNameFilter(ctx context.Context, containers []containerd.Container, valu
if err != nil {
return nil, err
}
- if strings.HasPrefix(info.ID, value) || strings.Contains(getPrintableContainerName(info.Labels), value) {
+ if strings.HasPrefix(info.ID, value) || strings.Contains(containerutil.GetContainerName(info.Labels), value) {
return &info, nil
}
}
diff --git a/pkg/cmd/container/logs.go b/pkg/cmd/container/logs.go
index c971a15d480..2deb143e6bc 100644
--- a/pkg/cmd/container/logs.go
+++ b/pkg/cmd/container/logs.go
@@ -23,16 +23,17 @@ import (
"os/signal"
"syscall"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/api/types/cri"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/labels/k8slabels"
- "github.com/containerd/nerdctl/pkg/logging"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/api/types/cri"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels"
+ "github.com/containerd/nerdctl/v2/pkg/logging"
)
func Logs(ctx context.Context, client *containerd.Client, container string, options types.ContainerLogsOptions) error {
@@ -43,7 +44,7 @@ func Logs(ctx context.Context, client *containerd.Client, container string, opti
switch options.GOptions.Namespace {
case "moby":
- logrus.Warn("Currently, `nerdctl logs` only supports containers created with `nerdctl run -d` or CRI")
+ log.G(ctx).Warn("Currently, `nerdctl logs` only supports containers created with `nerdctl run -d` or CRI")
}
stopChannel := make(chan os.Signal, 1)
@@ -90,7 +91,7 @@ func Logs(ctx context.Context, client *containerd.Client, container string, opti
// Setup goroutine to send stop event if container task finishes:
go func() {
<-waitCh
- logrus.Debugf("container task has finished, sending kill signal to log viewer")
+ log.G(ctx).Debugf("container task has finished, sending kill signal to log viewer")
stopChannel <- os.Interrupt
}()
}
diff --git a/pkg/cmd/container/pause.go b/pkg/cmd/container/pause.go
index 86cd5130298..99451a8e3e5 100644
--- a/pkg/cmd/container/pause.go
+++ b/pkg/cmd/container/pause.go
@@ -20,10 +20,11 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Pause pauses all containers specified by `reqs`.
@@ -38,7 +39,7 @@ func Pause(ctx context.Context, client *containerd.Client, reqs []string, option
return err
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(options.Stdout, found.Req)
return err
},
}
diff --git a/pkg/cmd/container/prune.go b/pkg/cmd/container/prune.go
index b53f9039dea..68614e0e42d 100644
--- a/pkg/cmd/container/prune.go
+++ b/pkg/cmd/container/prune.go
@@ -22,9 +22,10 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
// Prune remove all stopped containers
@@ -36,14 +37,14 @@ func Prune(ctx context.Context, client *containerd.Client, options types.Contain
var deleted []string
for _, c := range containers {
- if err = RemoveContainer(ctx, c, options.GOptions, false, true); err == nil {
+ if err = RemoveContainer(ctx, c, options.GOptions, false, true, client); err == nil {
deleted = append(deleted, c.ID())
continue
}
if errors.As(err, &ErrContainerStatus{}) {
continue
}
- logrus.WithError(err).Warnf("failed to remove container %s", c.ID())
+ log.G(ctx).WithError(err).Warnf("failed to remove container %s", c.ID())
}
if len(deleted) > 0 {
diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go
index 90c0ef5231e..d425b718972 100644
--- a/pkg/cmd/container/remove.go
+++ b/pkg/cmd/container/remove.go
@@ -22,22 +22,24 @@ import (
"errors"
"fmt"
"os"
- "runtime"
"syscall"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/namespaces"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/namestore"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/ipcutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+ "github.com/containerd/nerdctl/v2/pkg/namestore"
+ "github.com/containerd/nerdctl/v2/pkg/store"
)
var _ error = ErrContainerStatus{}
@@ -69,170 +71,246 @@ func Remove(ctx context.Context, client *containerd.Client, containers []string,
if found.MatchCount > 1 {
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
}
- if err := RemoveContainer(ctx, found.Container, options.GOptions, options.Force, options.Volumes); err != nil {
+ if err := RemoveContainer(ctx, found.Container, options.GOptions, options.Force, options.Volumes, client); err != nil {
if errors.As(err, &ErrContainerStatus{}) {
err = fmt.Errorf("%s. unpause/stop container first or force removal", err)
}
return err
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(options.Stdout, found.Req)
return err
},
}
err := walker.WalkAll(ctx, containers, true)
if err != nil && options.Force {
- logrus.Error(err)
+ log.G(ctx).Error(err)
return nil
}
return err
}
// RemoveContainer removes a container from containerd store.
-func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions types.GlobalCommandOptions, force bool, removeAnonVolumes bool) (retErr error) {
- // defer the storage of remove error in the dedicated label
+// It will first retrieve system objects (namestore, etcetera), then assess whether we should remove the container or not
+// based of "force" and the status of the task.
+// If we are to delete, it then kills and delete the task.
+// If task removal fails, we stop (except if it was just "NotFound").
+// We then enter the defer cleanup function that will:
+// - remove the network config (windows only)
+// - delete the container
+// - then and ONLY then, on a successful container remove, clean things-up on our side (volume store, etcetera)
+// If you do need to add more cleanup, please do so at the bottom of the defer function
+func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions types.GlobalCommandOptions, force bool, removeAnonVolumes bool, client *containerd.Client) (retErr error) {
+ // Get labels
+ containerLabels, err := c.Labels(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Get datastore
+ dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address)
+ if err != nil {
+ return err
+ }
+
+ // Ensure we do have a stateDir label
+ stateDir := containerLabels[labels.StateDir]
+ if stateDir == "" {
+ stateDir, err = containerutil.ContainerStateDirPath(globalOptions.Namespace, dataStore, c.ID())
+ if err != nil {
+ return err
+ }
+ }
+
+ // Lock the container state
+ lf, err := containerutil.Lock(stateDir)
+ if err != nil {
+ return err
+ }
+
defer func() {
+ // If there was an error, update the label
+ // Note that we will (obviously) not store any unlocking or statedir removal error from below
if retErr != nil {
containerutil.UpdateErrorLabel(ctx, c, retErr)
}
+ // Release the lock
+ retErr = errors.Join(lf.Release(), retErr)
+ // Note: technically, this is racy...
+ if retErr == nil {
+ retErr = os.RemoveAll(containerLabels[labels.StateDir])
+ }
}()
- ns, err := namespaces.NamespaceRequired(ctx)
+
+ // Get namespace
+ containerNamespace, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return err
}
- id := c.ID()
- l, err := c.Labels(ctx)
+
+ // Get namestore
+ nameStore, err := namestore.New(dataStore, containerNamespace)
if err != nil {
return err
}
- stateDir := l[labels.StateDir]
- name := l[labels.Name]
- dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address)
+ // Get volume store
+ volStore, err := volumestore.New(dataStore, globalOptions.Namespace)
if err != nil {
return err
}
- namst, err := namestore.New(dataStore, ns)
+ // Decode IPC
+ ipc, err := ipcutil.DecodeIPCLabel(containerLabels[labels.IPC])
if err != nil {
return err
}
+ // Get the container id and name
+ id := c.ID()
+ name := containerLabels[labels.Name]
+
+ // This will evaluate retErr to decide if we proceed with removal or not
defer func() {
- if errdefs.IsNotFound(retErr) {
- retErr = nil
- }
- if retErr != nil {
+ // If there was an error, and it was not "NotFound", this is a hard error, we stop here and do nothing.
+ if retErr != nil && !errdefs.IsNotFound(retErr) {
return
}
- if err := os.RemoveAll(stateDir); err != nil {
- logrus.WithError(retErr).Warnf("failed to remove container state dir %s", stateDir)
- }
- // enforce release name here in case the poststop hook name release fails
- if name != "" {
- if err := namst.Release(name, id); err != nil {
- logrus.WithError(retErr).Warnf("failed to release container name %s", name)
- }
+
+ // Otherwise, nil the error so that we do not write the error label on the container
+ retErr = nil
+
+ // Now, delete the actual container
+ var delOpts []containerd.DeleteOpts
+ if _, err := c.Image(ctx); err == nil {
+ delOpts = append(delOpts, containerd.WithSnapshotCleanup)
}
- if err := hostsstore.DeallocHostsFile(dataStore, ns, id); err != nil {
- logrus.WithError(retErr).Warnf("failed to remove hosts file for container %q", id)
+
+ spec, err := c.Spec(ctx)
+ if err != nil {
+ retErr = err
+ return
}
- }()
- // volume removal is not handled by the poststop hook lifecycle because it depends on removeAnonVolumes option
- if anonVolumesJSON, ok := l[labels.AnonymousVolumes]; ok && removeAnonVolumes {
- var anonVolumes []string
- if err := json.Unmarshal([]byte(anonVolumesJSON), &anonVolumes); err != nil {
- return err
+ netOpts, err := containerutil.NetworkOptionsFromSpec(spec)
+ if err != nil {
+ retErr = fmt.Errorf("failed to load container networking options from specs: %s", err)
+ return
}
- volStore, err := volume.Store(globalOptions.Namespace, globalOptions.DataRoot, globalOptions.Address)
+
+ networkManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netOpts, client)
if err != nil {
- return err
+ retErr = fmt.Errorf("failed to instantiate network options manager: %s", err)
+ return
}
- defer func() {
- if _, err := volStore.Remove(anonVolumes); err != nil {
- logrus.WithError(err).Warnf("failed to remove anonymous volumes %v", anonVolumes)
- }
- }()
- }
- task, err := c.Task(ctx, cio.Load)
- if err != nil {
- if errdefs.IsNotFound(err) {
- if c.Delete(ctx, containerd.WithSnapshotCleanup) != nil {
- return c.Delete(ctx)
- }
+ if err := networkManager.CleanupNetworking(ctx, c); err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to clean up container networking: %q", id)
}
- return err
- }
- status, err := task.Status(ctx)
- if err != nil {
- if errdefs.IsNotFound(err) {
- return nil
+ // Delete the container now. If it fails, try again without snapshot cleanup
+ // If it still fails, time to stop.
+ if c.Delete(ctx, delOpts...) != nil {
+ retErr = c.Delete(ctx)
+ if retErr != nil {
+ return
+ }
}
- return err
- }
- // NOTE: on non-Windows platforms, network cleanup is performed by OCI hooks.
- // Seeing as though Windows does not currently support OCI hooks, we must explicitly
- // perform the network cleanup from the main nerdctl executable.
- if runtime.GOOS == "windows" {
- spec, err := c.Spec(ctx)
- if err != nil {
- return err
+ // Container has been removed successfully. Now we just finish the cleanup on our side.
+
+ // Cleanup IPC - soft failure
+ if err = ipcutil.CleanUp(ipc); err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to cleanup IPC for container %q", id)
}
- netOpts, err := containerutil.NetworkOptionsFromSpec(spec)
- if err != nil {
- return fmt.Errorf("failed to load container networking options from specs: %s", err)
+ // Enforce release name here in case the poststop hook name release fails - soft failure
+ if name != "" {
+ // Double-releasing may happen with containers started with --rm, so, ignore NotFound errors
+ if err := nameStore.Release(name, id); err != nil && !errors.Is(err, store.ErrNotFound) {
+ log.G(ctx).WithError(err).Warnf("failed to release container name %s", name)
+ }
}
- networkManager, err := containerutil.NewNetworkingOptionsManager(globalOptions, netOpts)
+ hs, err := hostsstore.New(dataStore, containerNamespace)
if err != nil {
- return fmt.Errorf("failed to instantiate network options manager: %s", err)
+ log.G(ctx).WithError(err).Warnf("failed to instantiate hostsstore for %q", containerNamespace)
+ } else if err = hs.Delete(id); err != nil {
+ // De-allocate hosts file - soft failure
+ log.G(ctx).WithError(err).Warnf("failed to remove hosts file for container %q", id)
}
- if err := networkManager.CleanupNetworking(ctx, c); err != nil {
- logrus.WithError(retErr).Warnf("failed to clean up container networking: %s", err)
+ // Volume removal is not handled by the poststop hook lifecycle because it depends on removeAnonVolumes option
+ // Note that the anonymous volume list has been obtained earlier, without locking the volume store.
+ // Technically, a concurrent operation MAY have deleted these anonymous volumes already at this point, which
+ // would make this operation here "soft fail".
+ // This is not a problem per-se, though we will output a warning in that case.
+ if anonVolumesJSON, ok := containerLabels[labels.AnonymousVolumes]; ok && removeAnonVolumes {
+ var anonVolumes []string
+ if err = json.Unmarshal([]byte(anonVolumesJSON), &anonVolumes); err != nil {
+ log.G(ctx).WithError(err).Warnf("failed to unmarshall anonvolume information for container %q", id)
+ } else {
+ var errs []error
+ _, errs, err = volStore.Remove(func() ([]string, []error, error) {
+ return anonVolumes, nil, nil
+ })
+ if err != nil || len(errs) > 0 {
+ log.G(ctx).WithError(err).Warnf("failed to remove anonymous volumes %v", anonVolumes)
+ }
+ }
}
+ }()
+
+ // Get the task.
+ task, err := c.Task(ctx, cio.Load)
+ if err != nil {
+ return err
+ }
+
+ // Task was here, get the status
+ status, err := task.Status(ctx)
+ if err != nil {
+ return err
}
+ // Now, we have a live task with a status.
switch status.Status {
- case containerd.Created, containerd.Stopped:
- if _, err := task.Delete(ctx); err != nil && !errdefs.IsNotFound(err) {
- return fmt.Errorf("failed to delete task %v: %w", id, err)
- }
case containerd.Paused:
+ // Paused containers only get removed if we force
if !force {
return NewStatusError(id, status.Status)
}
- _, err := task.Delete(ctx, containerd.WithProcessKill)
- if err != nil && !errdefs.IsNotFound(err) {
- return fmt.Errorf("failed to delete task %v: %w", id, err)
- }
- // default is the case, when status.Status = containerd.Running
- default:
+ case containerd.Running:
+ // Running containers only get removed if we force
if !force {
return NewStatusError(id, status.Status)
}
- if err := task.Kill(ctx, syscall.SIGKILL); err != nil {
- logrus.WithError(err).Warnf("failed to send SIGKILL")
+ // Kill the task. Soft error.
+ if err = task.Kill(ctx, syscall.SIGKILL); err != nil && !errdefs.IsNotFound(err) {
+ log.G(ctx).WithError(err).Warnf("failed to send SIGKILL to task %v", id)
}
es, err := task.Wait(ctx)
if err == nil {
<-es
}
- _, err = task.Delete(ctx, containerd.WithProcessKill)
- if err != nil && !errdefs.IsNotFound(err) {
- logrus.WithError(err).Warnf("failed to delete task %v", id)
+ case containerd.Created:
+ // TODO(Iceber): Since `containerd.WithProcessKill` blocks the killing of tasks with PID 0,
+ // remove the judgment and break when it is compatible with the tasks.
+ if task.Pid() == 0 {
+ // Created tasks with PID 0 always get removed
+ // Delete the task, without forcing kill
+ _, err = task.Delete(ctx)
+ return err
}
- }
- var delOpts []containerd.DeleteOpts
- if _, err := c.Image(ctx); err == nil {
- delOpts = append(delOpts, containerd.WithSnapshotCleanup)
- }
-
- if err := c.Delete(ctx, delOpts...); err != nil {
+ case containerd.Stopped:
+ // Stopped containers always get removed
+ // Delete the task, without forcing kill
+ _, err = task.Delete(ctx)
return err
+ default:
+ // Unknown status error out
+ return fmt.Errorf("unknown container status %s", status.Status)
}
+
+ // Delete the task
+ _, err = task.Delete(ctx, containerd.WithProcessKill)
return err
}
diff --git a/pkg/cmd/container/rename.go b/pkg/cmd/container/rename.go
index 31b357e25e9..ca9ba6828e8 100644
--- a/pkg/cmd/container/rename.go
+++ b/pkg/cmd/container/rename.go
@@ -21,13 +21,15 @@ import (
"fmt"
"runtime"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/namestore"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/namestore"
)
// Rename change container name to a new name
@@ -42,7 +44,7 @@ func Rename(ctx context.Context, client *containerd.Client, containerID, newCont
if err != nil {
return err
}
- hostst, err := hostsstore.NewStore(dataStore)
+ hostst, err := hostsstore.New(dataStore, options.GOptions.Namespace)
if err != nil {
return err
}
@@ -66,24 +68,40 @@ func Rename(ctx context.Context, client *containerd.Client, containerID, newCont
}
func renameContainer(ctx context.Context, container containerd.Container, newName, ns string,
- namst namestore.NameStore, hostst hostsstore.Store) error {
+ namst namestore.NameStore, hostst hostsstore.Store) (err error) {
l, err := container.Labels(ctx)
if err != nil {
return err
}
name := l[labels.Name]
- if err := namst.Rename(name, container.ID(), newName); err != nil {
+
+ id := container.ID()
+
+ defer func() {
+ // If we errored, rollback whatever we can
+ if err != nil {
+ lbls := map[string]string{
+ labels.Name: name,
+ }
+ namst.Rename(newName, id, name)
+ hostst.Update(id, name)
+ container.SetLabels(ctx, lbls)
+ }
+ }()
+
+ if err = namst.Rename(name, id, newName); err != nil {
return err
}
if runtime.GOOS == "linux" {
- if err := hostst.Update(ns, container.ID(), newName); err != nil {
- return err
+ if err = hostst.Update(id, newName); err != nil {
+ log.G(ctx).WithError(err).Warn("failed to update host networking definitions " +
+ "- if your container is using network 'none', this is expected - otherwise, please report this as a bug")
}
}
- labels := map[string]string{
+ lbls := map[string]string{
labels.Name: newName,
}
- if _, err = container.SetLabels(ctx, labels); err != nil {
+ if _, err = container.SetLabels(ctx, lbls); err != nil {
return err
}
return nil
diff --git a/pkg/cmd/container/restart.go b/pkg/cmd/container/restart.go
index 068a11a8001..2be386ea323 100644
--- a/pkg/cmd/container/restart.go
+++ b/pkg/cmd/container/restart.go
@@ -20,10 +20,11 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Restart will restart one or more containers.
@@ -40,7 +41,7 @@ func Restart(ctx context.Context, client *containerd.Client, containers []string
if err := containerutil.Start(ctx, found.Container, false, client, ""); err != nil {
return err
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(options.Stdout, found.Req)
return err
},
}
diff --git a/pkg/cmd/container/run_cgroup_linux.go b/pkg/cmd/container/run_cgroup_linux.go
index 0bc34f1e94a..af43b12c1fd 100644
--- a/pkg/cmd/container/run_cgroup_linux.go
+++ b/pkg/cmd/container/run_cgroup_linux.go
@@ -23,14 +23,16 @@ import (
"path/filepath"
"strings"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
"github.com/docker/go-units"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
type customMemoryOptions struct {
@@ -41,11 +43,11 @@ type customMemoryOptions struct {
func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.SpecOpts, error) {
if options.KernelMemory != "" {
- logrus.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
+ log.L.Warnf("The --kernel-memory flag is no longer supported. This flag is a noop.")
}
if options.Memory == "" && options.OomKillDisable {
- logrus.Warn("Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
+ log.L.Warn("Disabling the OOM killer on containers without setting a '-m/--memory' limit may be dangerous.")
}
if options.GOptions.CgroupManager == "none" {
@@ -54,11 +56,11 @@ func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.
}
if options.CPUs > 0.0 || options.Memory != "" || options.MemorySwap != "" || options.PidsLimit > 0 {
- logrus.Warn(`cgroup manager is set to "none", discarding resource limit requests. ` +
+ log.L.Warn(`cgroup manager is set to "none", discarding resource limit requests. ` +
"(Hint: enable cgroup v2 with systemd: https://rootlesscontaine.rs/getting-started/common/cgroup2/)")
}
if options.CgroupParent != "" {
- logrus.Warnf(`cgroup manager is set to "none", ignoring cgroup parent %q`+
+ log.L.Warnf(`cgroup manager is set to "none", ignoring cgroup parent %q`+
"(Hint: enable cgroup v2 with systemd: https://rootlesscontaine.rs/getting-started/common/cgroup2/)", options.CgroupParent)
}
return []oci.SpecOpts{oci.WithCgroup("")}, nil
@@ -178,7 +180,7 @@ func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.
opts = append(opts, withUnified(unifieds))
if options.BlkioWeight != 0 && !infoutil.BlockIOWeight(options.GOptions.CgroupManager) {
- logrus.Warn("kernel support for cgroup blkio weight missing, weight discarded")
+ log.L.Warn("kernel support for cgroup blkio weight missing, weight discarded")
options.BlkioWeight = 0
}
if options.BlkioWeight > 0 && options.BlkioWeight < 10 || options.BlkioWeight > 1000 {
@@ -199,12 +201,13 @@ func generateCgroupOpts(id string, options types.ContainerCreateOptions) ([]oci.
}
for _, f := range options.Device {
- devPath, mode, err := ParseDevice(f)
+ devPath, conPath, mode, err := ParseDevice(f)
if err != nil {
return nil, fmt.Errorf("failed to parse device %q: %w", f, err)
}
- opts = append(opts, oci.WithLinuxDevice(devPath, mode))
+ opts = append(opts, oci.WithDevices(devPath, conPath, mode))
}
+
return opts, nil
}
@@ -246,8 +249,8 @@ func generateCgroupPath(id, cgroupManager, cgroupParent string) (string, error)
return path, nil
}
-// ParseDevice parses the give device string into hostDevPath and mode(defaults: "rwm").
-func ParseDevice(s string) (hostDevPath string, mode string, err error) {
+// ParseDevice parses the give device string into hostDevPath, containerPath and mode(defaults: "rwm").
+func ParseDevice(s string) (hostDevPath string, containerPath string, mode string, err error) {
mode = "rwm"
split := strings.Split(s, ":")
var containerDevPath string
@@ -268,21 +271,17 @@ func ParseDevice(s string) (hostDevPath string, mode string, err error) {
containerDevPath = split[1]
mode = split[2]
default:
- return "", "", errors.New("too many `:` symbols")
- }
-
- if containerDevPath != hostDevPath {
- return "", "", errors.New("changing the path inside the container is not supported yet")
+ return "", "", "", errors.New("too many `:` symbols")
}
if !filepath.IsAbs(hostDevPath) {
- return "", "", fmt.Errorf("%q is not an absolute path", hostDevPath)
+ return "", "", "", fmt.Errorf("%q is not an absolute path", hostDevPath)
}
if err := validateDeviceMode(mode); err != nil {
- return "", "", err
+ return "", "", "", err
}
- return hostDevPath, mode, nil
+ return hostDevPath, containerDevPath, mode, nil
}
func validateDeviceMode(mode string) error {
diff --git a/pkg/cmd/container/run_freebsd.go b/pkg/cmd/container/run_freebsd.go
index 5aabb440848..ddf743121e7 100644
--- a/pkg/cmd/container/run_freebsd.go
+++ b/pkg/cmd/container/run_freebsd.go
@@ -19,10 +19,11 @@ package container
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
func WithoutRunMount() func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error {
diff --git a/pkg/cmd/container/run_gpus.go b/pkg/cmd/container/run_gpus.go
index cd316fcb00c..967858b6bf5 100644
--- a/pkg/cmd/container/run_gpus.go
+++ b/pkg/cmd/container/run_gpus.go
@@ -22,10 +22,6 @@ import (
"fmt"
"strconv"
"strings"
-
- "github.com/containerd/containerd/contrib/nvidia"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
)
// GPUReq is a request for GPUs.
@@ -35,64 +31,6 @@ type GPUReq struct {
Capabilities []string
}
-func parseGPUOpts(value []string) (res []oci.SpecOpts, _ error) {
- for _, gpu := range value {
- gpuOpt, err := parseGPUOpt(gpu)
- if err != nil {
- return nil, err
- }
- res = append(res, gpuOpt)
- }
- return res, nil
-}
-
-func parseGPUOpt(value string) (oci.SpecOpts, error) {
- req, err := ParseGPUOptCSV(value)
- if err != nil {
- return nil, err
- }
-
- var gpuOpts []nvidia.Opts
-
- if len(req.DeviceIDs) > 0 {
- gpuOpts = append(gpuOpts, nvidia.WithDeviceUUIDs(req.DeviceIDs...))
- } else if req.Count > 0 {
- var devices []int
- for i := 0; i < req.Count; i++ {
- devices = append(devices, i)
- }
- gpuOpts = append(gpuOpts, nvidia.WithDevices(devices...))
- } else if req.Count < 0 {
- gpuOpts = append(gpuOpts, nvidia.WithAllDevices)
- }
-
- str2cap := make(map[string]nvidia.Capability)
- for _, c := range nvidia.AllCaps() {
- str2cap[string(c)] = c
- }
- var nvidiaCaps []nvidia.Capability
- for _, c := range req.Capabilities {
- if cap, isNvidiaCap := str2cap[c]; isNvidiaCap {
- nvidiaCaps = append(nvidiaCaps, cap)
- }
- }
- if len(nvidiaCaps) != 0 {
- gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidiaCaps...))
- } else {
- // Add "utility", "compute" capability if unset.
- // Please see also: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities
- gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidia.Utility, nvidia.Compute))
- }
-
- if rootlessutil.IsRootless() {
- // "--no-cgroups" option is needed to nvidia-container-cli in rootless environment
- // Please see also: https://github.com/moby/moby/issues/38729#issuecomment-463493866
- gpuOpts = append(gpuOpts, nvidia.WithNoCgroups)
- }
-
- return nvidia.WithGPUs(gpuOpts...), nil
-}
-
// ParseGPUOptCSV parses a GPU option from CSV.
func ParseGPUOptCSV(value string) (*GPUReq, error) {
csvReader := csv.NewReader(strings.NewReader(value))
diff --git a/pkg/cmd/container/run_linux.go b/pkg/cmd/container/run_linux.go
index cb90dbf694a..cbe38d62e46 100644
--- a/pkg/cmd/container/run_linux.go
+++ b/pkg/cmd/container/run_linux.go
@@ -21,19 +21,22 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/userns"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/bypass4netnsutil"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/docker/go-units"
+ "github.com/moby/sys/userns"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/contrib/nvidia"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/bypass4netnsutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/ipcutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// WithoutRunMount returns a SpecOpts that unmounts the default tmpfs on "/run"
@@ -59,10 +62,7 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts
}
opts = append(opts, cgOpts...)
- labelsMap, err := readKVStringsMapfFromLabel(options.Label, options.LabelFile)
- if err != nil {
- return nil, err
- }
+ annotations := strutil.ConvertKVStringsToMap(options.Annotations)
capOpts, err := generateCapOpts(
strutil.DedupeStrSlice(options.CapAdd),
@@ -78,23 +78,23 @@ func setPlatformOptions(ctx context.Context, client *containerd.Client, id, uts
}
opts = append(opts, secOpts...)
- b4nnOpts, err := bypass4netnsutil.GenerateBypass4netnsOpts(securityOptsMaps, labelsMap, id)
+ b4nnOpts, err := bypass4netnsutil.GenerateBypass4netnsOpts(securityOptsMaps, annotations, id)
if err != nil {
return nil, err
}
opts = append(opts, b4nnOpts...)
- if len(options.ShmSize) > 0 {
- shmBytes, err := units.RAMInBytes(options.ShmSize)
- if err != nil {
- return nil, err
- }
- opts = append(opts, oci.WithDevShmSize(shmBytes/1024))
- }
ulimitOpts, err := generateUlimitsOpts(options.Ulimit)
if err != nil {
return nil, err
}
+
+ // If without any ulimitOpts, we need to reset the default value from spec
+ // which has 1024 as file limit. Make this behavior same as containerd/cri.
+ if len(ulimitOpts) == 0 {
+ ulimitOpts = append(ulimitOpts, withRlimits(nil))
+ }
+
opts = append(opts, ulimitOpts...)
if options.Sysctl != nil {
opts = append(opts, WithSysctls(strutil.ConvertKVStringsToMap(options.Sysctl)))
@@ -142,15 +142,13 @@ func generateNamespaceOpts(
return nil, fmt.Errorf("unknown uts value. valid value(s) are 'host', got: %q", uts)
}
- switch options.IPC {
- case "host":
- opts = append(opts, oci.WithHostNamespace(specs.IPCNamespace))
- opts = append(opts, withBindMountHostIPC)
- case "private", "":
- // If nothing is specified, or if private, default to normal behavior
- default:
- return nil, fmt.Errorf("unknown ipc value. valid values are 'private' or 'host', got: %q", options.IPC)
+ stateDir := internalLabels.stateDir
+ ipcOpts, ipcLabel, err := generateIPCOpts(ctx, client, options.IPC, options.ShmSize, stateDir)
+ if err != nil {
+ return nil, err
}
+ internalLabels.ipc = ipcLabel
+ opts = append(opts, ipcOpts...)
pidOpts, pidLabel, err := generatePIDOpts(ctx, client, options.Pid)
if err != nil {
@@ -162,6 +160,25 @@ func generateNamespaceOpts(
return opts, nil
}
+func generateIPCOpts(ctx context.Context, client *containerd.Client, ipcFlag string, shmSize string, stateDir string) ([]oci.SpecOpts, string, error) {
+ ipcFlag = strings.ToLower(ipcFlag)
+
+ ipc, err := ipcutil.DetectFlags(ctx, client, stateDir, ipcFlag, shmSize)
+ if err != nil {
+ return nil, "", err
+ }
+ ipcLabel, err := ipcutil.EncodeIPCLabel(ipc)
+ if err != nil {
+ return nil, "", err
+ }
+ opts, err := ipcutil.GenerateIPCOpts(ctx, ipc, client)
+ if err != nil {
+ return nil, "", err
+ }
+
+ return opts, ipcLabel, nil
+}
+
func generatePIDOpts(ctx context.Context, client *containerd.Client, pid string) ([]oci.SpecOpts, string, error) {
opts := make([]oci.SpecOpts, 0)
pid = strings.ToLower(pid)
@@ -229,7 +246,7 @@ func setOOMScoreAdj(opts []oci.SpecOpts, oomScoreAdjChanged bool, oomScoreAdj in
// (FIXME: find a more robust way to get the current minimum value)
const minimum = 100
if oomScoreAdj < minimum {
- logrus.Warnf("Limiting oom_score_adj (%d -> %d)", oomScoreAdj, minimum)
+ log.L.Warnf("Limiting oom_score_adj (%d -> %d)", oomScoreAdj, minimum)
oomScoreAdj = minimum
}
}
@@ -244,3 +261,61 @@ func withOOMScoreAdj(score int) oci.SpecOpts {
return nil
}
}
+
+func parseGPUOpts(value []string) (res []oci.SpecOpts, _ error) {
+ for _, gpu := range value {
+ gpuOpt, err := parseGPUOpt(gpu)
+ if err != nil {
+ return nil, err
+ }
+ res = append(res, gpuOpt)
+ }
+ return res, nil
+}
+
+func parseGPUOpt(value string) (oci.SpecOpts, error) {
+ req, err := ParseGPUOptCSV(value)
+ if err != nil {
+ return nil, err
+ }
+
+ var gpuOpts []nvidia.Opts
+
+ if len(req.DeviceIDs) > 0 {
+ gpuOpts = append(gpuOpts, nvidia.WithDeviceUUIDs(req.DeviceIDs...))
+ } else if req.Count > 0 {
+ var devices []int
+ for i := 0; i < req.Count; i++ {
+ devices = append(devices, i)
+ }
+ gpuOpts = append(gpuOpts, nvidia.WithDevices(devices...))
+ } else if req.Count < 0 {
+ gpuOpts = append(gpuOpts, nvidia.WithAllDevices)
+ }
+
+ str2cap := make(map[string]nvidia.Capability)
+ for _, c := range nvidia.AllCaps() {
+ str2cap[string(c)] = c
+ }
+ var nvidiaCaps []nvidia.Capability
+ for _, c := range req.Capabilities {
+ if cp, isNvidiaCap := str2cap[c]; isNvidiaCap {
+ nvidiaCaps = append(nvidiaCaps, cp)
+ }
+ }
+ if len(nvidiaCaps) != 0 {
+ gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidiaCaps...))
+ } else {
+ // Add "utility", "compute" capability if unset.
+ // Please see also: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/user-guide.html#driver-capabilities
+ gpuOpts = append(gpuOpts, nvidia.WithCapabilities(nvidia.Utility, nvidia.Compute))
+ }
+
+ if rootlessutil.IsRootless() {
+ // "--no-cgroups" option is needed to nvidia-container-cli in rootless environment
+ // Please see also: https://github.com/moby/moby/issues/38729#issuecomment-463493866
+ gpuOpts = append(gpuOpts, nvidia.WithNoCgroups)
+ }
+
+ return nvidia.WithGPUs(gpuOpts...), nil
+}
diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go
index 9bfd6540399..eb9be3aefb2 100644
--- a/pkg/cmd/container/run_mount.go
+++ b/pkg/cmd/container/run_mount.go
@@ -18,31 +18,38 @@ package container
import (
"context"
+ "encoding/json"
+ "errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
+ "time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/mount"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/userns"
- "github.com/containerd/continuity/fs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
- "github.com/containerd/nerdctl/pkg/idgen"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/mountutil"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
- "github.com/containerd/nerdctl/pkg/strutil"
securejoin "github.com/cyphar/filepath-securejoin"
+ "github.com/moby/sys/userns"
"github.com/opencontainers/image-spec/identity"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/core/leases"
+ "github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/continuity/fs"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// copy from https://github.com/containerd/containerd/blob/v1.6.0-rc.1/pkg/cri/opts/spec_linux.go#L129-L151
@@ -88,7 +95,8 @@ func withMounts(mounts []specs.Mount) oci.SpecOpts {
func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]*mountutil.Processed, error) {
var parsed []*mountutil.Processed //nolint:prealloc
for _, v := range strutil.DedupeStrSlice(options.Volume) {
- x, err := mountutil.ProcessFlagV(v, volStore)
+ // createDir=true for -v option to allow creation of directory on host if not found.
+ x, err := mountutil.ProcessFlagV(v, volStore, true)
if err != nil {
return nil, err
}
@@ -116,13 +124,8 @@ func parseMountFlags(volStore volumestore.VolumeStore, options types.ContainerCr
// generateMountOpts generates volume-related mount opts.
// Other mounts such as procfs mount are not handled here.
-func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredImage *imgutil.EnsuredImage, options types.ContainerCreateOptions) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) {
- // volume store is corresponds to a directory like `/var/lib/nerdctl/1935db59/volumes/default`
- volStore, err := volume.Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
- if err != nil {
- return nil, nil, nil, err
- }
-
+func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredImage *imgutil.EnsuredImage,
+ volStore volumestore.VolumeStore, options types.ContainerCreateOptions) ([]oci.SpecOpts, []string, []*mountutil.Processed, error) {
//nolint:golint,prealloc
var (
opts []oci.SpecOpts
@@ -156,6 +159,14 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
// When the Unmount fails, RemoveAll will incorrectly delete data from the mounted dir
defer os.Remove(tempDir)
+ // Add a lease of 1 hour to the view so that it is not garbage collected
+ // Note(gsamfira): should we make this shorter?
+ ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("failed to create lease: %w", err)
+ }
+ defer done(ctx)
+
var mounts []mount.Mount
mounts, err = s.View(ctx, tempDir, chainID)
if err != nil {
@@ -166,26 +177,14 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
// https://github.com/containerd/containerd/commit/791e175c79930a34cfbb2048fbcaa8493fd2c86b
unmounter := func(mountPath string) {
if uerr := mount.Unmount(mountPath, 0); uerr != nil {
- logrus.Debugf("Failed to unmount snapshot %q", tempDir)
+ log.G(ctx).Debugf("Failed to unmount snapshot %q", tempDir)
if err == nil {
err = uerr
}
}
}
- if runtime.GOOS == "windows" {
- for _, m := range mounts {
- defer unmounter(m.Source)
- // appending the layerID to the root.
- mountPath := filepath.Join(tempDir, filepath.Base(m.Source))
- if err := m.Mount(mountPath); err != nil {
- if err := s.Remove(ctx, tempDir); err != nil && !errdefs.IsNotFound(err) {
- return nil, nil, nil, err
- }
- return nil, nil, nil, err
- }
- }
- } else if runtime.GOOS == "linux" {
+ if runtime.GOOS == "linux" {
defer unmounter(tempDir)
for _, m := range mounts {
m := m
@@ -257,9 +256,9 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
}
anonVolName := idgen.GenerateID()
- logrus.Debugf("creating anonymous volume %q, for \"VOLUME %s\"",
+ log.G(ctx).Debugf("creating anonymous volume %q, for \"VOLUME %s\"",
anonVolName, imgVolRaw)
- anonVol, err := volStore.Create(anonVolName, []string{})
+ anonVol, err := volStore.CreateWithoutLock(anonVolName, []string{})
if err != nil {
return nil, nil, nil, err
}
@@ -292,6 +291,58 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm
}
opts = append(opts, withMounts(userMounts))
+
+ containers, err := client.Containers(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ vfSet := strutil.SliceToSet(options.VolumesFrom)
+ var vfMountPoints []dockercompat.MountPoint
+ var vfAnonVolumes []string
+
+ for _, c := range containers {
+ ls, err := c.Labels(ctx)
+ if err != nil {
+ // Containerd note: there is no guarantee that the containers we got from the list still exist at this point
+ // If that is the case, just ignore and move on
+ if errors.Is(err, errdefs.ErrNotFound) {
+ log.G(ctx).Debugf("container %q is gone - ignoring", c.ID())
+ continue
+ }
+ return nil, nil, nil, err
+ }
+ _, idMatch := vfSet[c.ID()]
+ nameMatch := false
+ if name, found := ls[labels.Name]; found {
+ _, nameMatch = vfSet[name]
+ }
+
+ if idMatch || nameMatch {
+ if av, found := ls[labels.AnonymousVolumes]; found {
+ err = json.Unmarshal([]byte(av), &vfAnonVolumes)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ }
+ if m, found := ls[labels.Mounts]; found {
+ err = json.Unmarshal([]byte(m), &vfMountPoints)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ }
+
+ ps := processeds(vfMountPoints)
+ s, err := c.Spec(ctx)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ opts = append(opts, withMounts(s.Mounts))
+ anonVolumes = append(anonVolumes, vfAnonVolumes...)
+ mountPoints = append(mountPoints, ps...)
+ }
+ }
+
return opts, anonVolumes, mountPoints, nil
}
@@ -306,7 +357,7 @@ func copyExistingContents(source, destination string) error {
return err
}
if len(dstList) != 0 {
- logrus.Debugf("volume at %q is not initially empty, skipping copying", destination)
+ log.L.Debugf("volume at %q is not initially empty, skipping copying", destination)
return nil
}
return fs.CopyDir(destination, source)
diff --git a/pkg/cmd/container/run_restart.go b/pkg/cmd/container/run_restart.go
index 81c190133c6..5932f2175d5 100644
--- a/pkg/cmd/container/run_restart.go
+++ b/pkg/cmd/container/run_restart.go
@@ -21,9 +21,10 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/runtime/restart"
- "github.com/containerd/nerdctl/pkg/strutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/runtime/restart"
+
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func checkRestartCapabilities(ctx context.Context, client *containerd.Client, restartFlag string) (bool, error) {
@@ -32,7 +33,7 @@ func checkRestartCapabilities(ctx context.Context, client *containerd.Client, re
case "", "no":
return true, nil
}
- res, err := client.IntrospectionService().Plugins(ctx, []string{"id==restart"})
+ res, err := client.IntrospectionService().Plugins(ctx, "id==restart")
if err != nil {
return false, err
}
diff --git a/pkg/cmd/container/run_runtime.go b/pkg/cmd/container/run_runtime.go
index 20a121d5dad..b6d0ac4965e 100644
--- a/pkg/cmd/container/run_runtime.go
+++ b/pkg/cmd/container/run_runtime.go
@@ -20,17 +20,18 @@ import (
"context"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/plugin"
- runcoptions "github.com/containerd/containerd/runtime/v2/runc/options"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ runcoptions "github.com/containerd/containerd/api/types/runc/options"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/containerd/v2/plugins"
+ "github.com/containerd/log"
)
func generateRuntimeCOpts(cgroupManager, runtimeStr string) ([]containerd.NewContainerOpts, error) {
- runtime := plugin.RuntimeRuncV2
+ runtime := plugins.RuntimeRuncV2
var (
runcOpts runcoptions.Options
runtimeOpts interface{} = &runcOpts
@@ -43,7 +44,7 @@ func generateRuntimeCOpts(cgroupManager, runtimeStr string) ([]containerd.NewCon
runtime = runtimeStr
if !strings.HasPrefix(runtimeStr, "io.containerd.runc.") {
if cgroupManager == "systemd" {
- logrus.Warnf("cannot set cgroup manager to %q for runtime %q", cgroupManager, runtimeStr)
+ log.L.Warnf("cannot set cgroup manager to %q for runtime %q", cgroupManager, runtimeStr)
}
runtimeOpts = nil
}
diff --git a/pkg/cmd/container/run_security_linux.go b/pkg/cmd/container/run_security_linux.go
index a325c91b87c..510310f265a 100644
--- a/pkg/cmd/container/run_security_linux.go
+++ b/pkg/cmd/container/run_security_linux.go
@@ -21,15 +21,16 @@ import (
"strings"
"sync"
- "github.com/containerd/containerd/contrib/apparmor"
- "github.com/containerd/containerd/contrib/seccomp"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/cap"
- "github.com/containerd/nerdctl/pkg/apparmorutil"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/maputil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/contrib/apparmor"
+ "github.com/containerd/containerd/v2/contrib/seccomp"
+ "github.com/containerd/containerd/v2/pkg/cap"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/maputil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
var privilegedOpts = []oci.SpecOpts{
@@ -44,16 +45,20 @@ var privilegedWithoutDevicesOpts = []oci.SpecOpts{
oci.WithNewPrivileges,
}
+const (
+ systemPathsUnconfined = "unconfined"
+)
+
func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([]oci.SpecOpts, error) {
for k := range securityOptsMap {
switch k {
- case "seccomp", "apparmor", "no-new-privileges", "privileged-without-host-devices":
+ case "seccomp", "apparmor", "no-new-privileges", "systempaths", "privileged-without-host-devices":
default:
- logrus.Warnf("unknown security-opt: %q", k)
+ log.L.Warnf("unknown security-opt: %q", k)
}
}
var opts []oci.SpecOpts
- if seccompProfile, ok := securityOptsMap["seccomp"]; ok {
+ if seccompProfile, ok := securityOptsMap["seccomp"]; ok && seccompProfile != defaults.SeccompProfileName {
if seccompProfile == "" {
return nil, errors.New("invalid security-opt \"seccomp\"")
}
@@ -73,7 +78,7 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
}
if aaProfile != "unconfined" {
if !canApplyExistingProfile {
- logrus.Warnf("the host does not support AppArmor. Ignoring profile %q", aaProfile)
+ log.L.Warnf("the host does not support AppArmor. Ignoring profile %q", aaProfile)
} else {
opts = append(opts, apparmor.WithProfile(aaProfile))
}
@@ -98,6 +103,13 @@ func generateSecurityOpts(privileged bool, securityOptsMap map[string]string) ([
opts = append(opts, oci.WithNewPrivileges)
}
+ if value, ok := securityOptsMap["systempaths"]; ok && value == systemPathsUnconfined {
+ opts = append(opts, oci.WithMaskedPaths(nil))
+ opts = append(opts, oci.WithReadonlyPaths(nil))
+ } else if ok && value != systemPathsUnconfined {
+ return nil, errors.New(`invalid security-opt "systempaths=unconfined"`)
+ }
+
privilegedWithoutHostDevices, err := maputil.MapBoolValueAsOpt(securityOptsMap, "privileged-without-host-devices")
if err != nil {
return nil, err
@@ -127,7 +139,7 @@ func canonicalizeCapName(s string) string {
s = "CAP_" + s
}
if !isKnownCapName(s) {
- logrus.Warnf("unknown capability name %q", s)
+ log.L.Warnf("unknown capability name %q", s)
// Not a fatal error, because runtime might be aware of this cap
}
return s
diff --git a/pkg/cmd/container/run_ulimit.go b/pkg/cmd/container/run_ulimit_linux.go
similarity index 90%
rename from pkg/cmd/container/run_ulimit.go
rename to pkg/cmd/container/run_ulimit_linux.go
index 52fec1f8ba6..b19c31a35dc 100644
--- a/pkg/cmd/container/run_ulimit.go
+++ b/pkg/cmd/container/run_ulimit_linux.go
@@ -20,11 +20,13 @@ import (
"context"
"strings"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/docker/go-units"
"github.com/opencontainers/runtime-spec/specs-go"
+
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func generateUlimitsOpts(ulimits []string) ([]oci.SpecOpts, error) {
diff --git a/pkg/cmd/container/run_user.go b/pkg/cmd/container/run_user.go
index 7bc9324694b..f03342897e5 100644
--- a/pkg/cmd/container/run_user.go
+++ b/pkg/cmd/container/run_user.go
@@ -21,8 +21,8 @@ import (
"fmt"
"strconv"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
)
func generateUserOpts(user string) ([]oci.SpecOpts, error) {
diff --git a/pkg/cmd/container/run_windows.go b/pkg/cmd/container/run_windows.go
index 6d6e5a32184..6b38e7e9f3f 100644
--- a/pkg/cmd/container/run_windows.go
+++ b/pkg/cmd/container/run_windows.go
@@ -22,12 +22,14 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
"github.com/docker/go-units"
"github.com/opencontainers/runtime-spec/specs-go"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
const (
@@ -47,7 +49,7 @@ func setPlatformOptions(
internalLabels *internalLabels,
options types.ContainerCreateOptions,
) ([]oci.SpecOpts, error) {
- var opts []oci.SpecOpts
+ opts := []oci.SpecOpts{}
if options.CPUs > 0.0 {
opts = append(opts, oci.WithWindowsCPUCount(uint64(options.CPUs)))
}
diff --git a/pkg/cmd/container/start.go b/pkg/cmd/container/start.go
index c4b98e8d09b..3d9de68cb29 100644
--- a/pkg/cmd/container/start.go
+++ b/pkg/cmd/container/start.go
@@ -20,10 +20,11 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Start starts a list of `containers`. If attach is true, it only starts a single container.
@@ -43,7 +44,7 @@ func Start(ctx context.Context, client *containerd.Client, reqs []string, option
return err
}
if !options.Attach {
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(options.Stdout, found.Req)
if err != nil {
return err
}
diff --git a/cmd/nerdctl/container_stats.go b/pkg/cmd/container/stats.go
similarity index 69%
rename from cmd/nerdctl/container_stats.go
rename to pkg/cmd/container/stats.go
index 63fc5b37515..b21c08fce61 100644
--- a/cmd/nerdctl/container_stats.go
+++ b/pkg/cmd/container/stats.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"bytes"
@@ -27,46 +27,24 @@ import (
"text/template"
"time"
- "github.com/containerd/containerd"
eventstypes "github.com/containerd/containerd/api/events"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/events"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/containerinspector"
- "github.com/containerd/nerdctl/pkg/eventutil"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/statsutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/events"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
"github.com/containerd/typeurl/v2"
- "github.com/sirupsen/logrus"
- "github.com/spf13/cobra"
-)
-
-func newStatsCommand() *cobra.Command {
- var statsCommand = &cobra.Command{
- Use: "stats",
- Short: "Display a live stream of container(s) resource usage statistics.",
- RunE: statsAction,
- ValidArgsFunction: statsShellComplete,
- SilenceUsage: true,
- SilenceErrors: true,
- }
-
- addStatsFlags(statsCommand)
- return statsCommand
-}
-
-func addStatsFlags(cmd *cobra.Command) {
- cmd.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
- cmd.Flags().String("format", "", "Pretty-print images using a Go template, e.g, '{{json .}}'")
- cmd.Flags().Bool("no-stream", false, "Disable streaming stats and only pull the first result")
- cmd.Flags().Bool("no-trunc", false, "Do not truncate output")
-}
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerinspector"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/eventutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/statsutil"
+)
type stats struct {
mu sync.Mutex
@@ -77,7 +55,7 @@ type stats struct {
func (s *stats) add(cs *statsutil.Stats) bool {
s.mu.Lock()
defer s.mu.Unlock()
- if _, exists := s.isKnownContainer(cs.Container); !exists {
+ if _, exists := s.isKnownContainer(cs.ID); !exists {
s.cs = append(s.cs, cs)
return true
}
@@ -96,69 +74,42 @@ func (s *stats) remove(id string) {
// isKnownContainer is from https://github.com/docker/cli/blob/3fb4fb83dfb5db0c0753a8316f21aea54dab32c5/cli/command/container/stats_helpers.go#L44-L51
func (s *stats) isKnownContainer(cid string) (int, bool) {
for i, c := range s.cs {
- if c.Container == cid {
+ if c.ID == cid {
return i, true
}
}
return -1, false
}
-func statsAction(cmd *cobra.Command, args []string) error {
-
+// Stats displays a live stream of container(s) resource usage statistics.
+func Stats(ctx context.Context, client *containerd.Client, containerIDs []string, options types.ContainerStatsOptions) error {
// NOTE: rootless container does not rely on cgroupv1.
// more details about possible ways to resolve this concern: #223
- globalOptions, err := processRootCmdFlags(cmd)
- if err != nil {
- return err
- }
if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" {
return errors.New("stats requires cgroup v2 for rootless containers, see https://rootlesscontaine.rs/getting-started/common/cgroup2/")
}
- showAll := len(args) == 0
+ showAll := len(containerIDs) == 0
closeChan := make(chan error)
- all, err := cmd.Flags().GetBool("all")
- if err != nil {
- return err
- }
-
- noStream, err := cmd.Flags().GetBool("no-stream")
- if err != nil {
- return err
- }
-
- format, err := cmd.Flags().GetString("format")
- if err != nil {
- return err
- }
- var w = cmd.OutOrStdout()
+ var err error
+ var w = options.Stdout
var tmpl *template.Template
- switch format {
+ switch options.Format {
case "", "table":
- w = tabwriter.NewWriter(cmd.OutOrStdout(), 10, 1, 3, ' ', 0)
+ w = tabwriter.NewWriter(options.Stdout, 10, 1, 3, ' ', 0)
case "raw":
return errors.New("unsupported format: \"raw\"")
default:
- tmpl, err = formatter.ParseTemplate(format)
+ tmpl, err = formatter.ParseTemplate(options.Format)
if err != nil {
return err
}
}
- noTrunc, err := cmd.Flags().GetBool("no-trunc")
- if err != nil {
- return err
- }
-
// waitFirst is a WaitGroup to wait first stat data's reach for each container
waitFirst := &sync.WaitGroup{}
cStats := stats{}
- client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
- if err != nil {
- return err
- }
- defer cancel()
monitorContainerEvents := func(started chan<- struct{}, c chan *events.Envelope) {
eventsClient := client.EventService()
@@ -189,7 +140,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
for _, c := range containers {
cStatus := formatter.ContainerStatus(ctx, c)
- if !all {
+ if !options.All {
if !strings.HasPrefix(cStatus, "Up") {
continue
}
@@ -197,7 +148,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
s := statsutil.NewStats(c.ID())
if cStats.add(s) {
waitFirst.Add(1)
- go collect(cmd, globalOptions, s, waitFirst, c.ID(), !noStream)
+ go collect(ctx, options.GOptions, s, waitFirst, c.ID(), !options.NoStream)
}
}
}
@@ -228,7 +179,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
s := statsutil.NewStats(datacc.ID)
if cStats.add(s) {
waitFirst.Add(1)
- go collect(cmd, globalOptions, s, waitFirst, datacc.ID, !noStream)
+ go collect(ctx, options.GOptions, s, waitFirst, datacc.ID, !options.NoStream)
}
})
@@ -271,13 +222,13 @@ func statsAction(cmd *cobra.Command, args []string) error {
s := statsutil.NewStats(found.Container.ID())
if cStats.add(s) {
waitFirst.Add(1)
- go collect(cmd, globalOptions, s, waitFirst, found.Container.ID(), !noStream)
+ go collect(ctx, options.GOptions, s, waitFirst, found.Container.ID(), !options.NoStream)
}
return nil
},
}
- if err := walker.WalkAll(ctx, args, false); err != nil {
+ if err := walker.WalkAll(ctx, containerIDs, false); err != nil {
return err
}
@@ -287,9 +238,9 @@ func statsAction(cmd *cobra.Command, args []string) error {
}
cleanScreen := func() {
- if !noStream {
- fmt.Fprint(cmd.OutOrStdout(), "\033[2J")
- fmt.Fprint(cmd.OutOrStdout(), "\033[H")
+ if !options.NoStream {
+ fmt.Fprint(options.Stdout, "\033[2J")
+ fmt.Fprint(options.Stdout, "\033[H")
}
}
@@ -305,7 +256,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
cStats.mu.Lock()
for _, c := range cStats.cs {
if err := c.GetError(); err != nil {
- fmt.Fprintf(cmd.ErrOrStderr(), "unable to get stat entry: %s\n", err)
+ fmt.Fprintf(options.Stderr, "unable to get stat entry: %s\n", err)
}
ccstats = append(ccstats, c.GetStatistics())
}
@@ -313,7 +264,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
if !firstTick {
// print header for every tick
- if format == "" || format == "table" {
+ if options.Format == "" || options.Format == "table" {
fmt.Fprintln(w, "CONTAINER ID\tNAME\tCPU %\tMEM USAGE / LIMIT\tMEM %\tNET I/O\tBLOCK I/O\tPIDS")
}
}
@@ -322,14 +273,14 @@ func statsAction(cmd *cobra.Command, args []string) error {
if c.ID == "" {
continue
}
- rc := statsutil.RenderEntry(&c, noTrunc)
+ rc := statsutil.RenderEntry(&c, options.NoTrunc)
if !firstTick {
if tmpl != nil {
var b bytes.Buffer
if err := tmpl.Execute(&b, rc); err != nil {
break
}
- if _, err = fmt.Fprintln(cmd.OutOrStdout(), b.String()); err != nil {
+ if _, err = fmt.Fprintln(options.Stdout, b.String()); err != nil {
break
}
} else {
@@ -355,7 +306,7 @@ func statsAction(cmd *cobra.Command, args []string) error {
if len(cStats.cs) == 0 && !showAll {
break
}
- if noStream && !firstTick {
+ if options.NoStream && !firstTick {
break
}
select {
@@ -374,8 +325,8 @@ func statsAction(cmd *cobra.Command, args []string) error {
return err
}
-func collect(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, s *statsutil.Stats, waitFirst *sync.WaitGroup, id string, noStream bool) {
- logrus.Debugf("collecting stats for %s", s.Container)
+func collect(ctx context.Context, globalOptions types.GlobalCommandOptions, s *statsutil.Stats, waitFirst *sync.WaitGroup, id string, noStream bool) {
+ log.G(ctx).Debugf("collecting stats for %s", s.ID)
var (
getFirst = true
u = make(chan error, 1)
@@ -388,12 +339,15 @@ func collect(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, s *st
waitFirst.Done()
}
}()
- client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
+ client, ctx, cancel, err := clientutil.NewClient(ctx, globalOptions.Namespace, globalOptions.Address)
if err != nil {
s.SetError(err)
return
}
- defer cancel()
+ defer func() {
+ cancel()
+ client.Close()
+ }()
container, err := client.LoadContainer(ctx, id)
if err != nil {
s.SetError(err)
@@ -441,7 +395,7 @@ func collect(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, s *st
u <- err
continue
}
- statsEntry.Name = clabels[labels.Name]
+ statsEntry.Name = containerutil.GetContainerName(clabels)
statsEntry.ID = container.ID()
if firstSet {
@@ -480,11 +434,3 @@ func collect(cmd *cobra.Command, globalOptions types.GlobalCommandOptions, s *st
}
}
}
-
-func statsShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- // show running container names
- statusFilterFn := func(st containerd.ProcessStatus) bool {
- return st == containerd.Running
- }
- return shellCompleteContainerNames(cmd, statusFilterFn)
-}
diff --git a/cmd/nerdctl/container_stats_freebsd.go b/pkg/cmd/container/stats_freebsd.go
similarity index 86%
rename from cmd/nerdctl/container_stats_freebsd.go
rename to pkg/cmd/container/stats_freebsd.go
index a6460b3218a..ef2c98fdfad 100644
--- a/cmd/nerdctl/container_stats_freebsd.go
+++ b/pkg/cmd/container/stats_freebsd.go
@@ -14,11 +14,11 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/statsutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/statsutil"
)
func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface) (statsutil.StatsEntry, error) {
diff --git a/cmd/nerdctl/container_stats_linux.go b/pkg/cmd/container/stats_linux.go
similarity index 95%
rename from cmd/nerdctl/container_stats_linux.go
rename to pkg/cmd/container/stats_linux.go
index a8d29deec3f..76aa1c96ab3 100644
--- a/cmd/nerdctl/container_stats_linux.go
+++ b/pkg/cmd/container/stats_linux.go
@@ -14,7 +14,7 @@
limitations under the License.
*/
-package main
+package container
import (
"errors"
@@ -23,12 +23,14 @@ import (
"strings"
"time"
- v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
- v2 "github.com/containerd/cgroups/v3/cgroup2/stats"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/statsutil"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netns"
+
+ v1 "github.com/containerd/cgroups/v3/cgroup1/stats"
+ v2 "github.com/containerd/cgroups/v3/cgroup2/stats"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/statsutil"
)
//nolint:nakedret
diff --git a/cmd/nerdctl/container_stats_windows.go b/pkg/cmd/container/stats_windows.go
similarity index 86%
rename from cmd/nerdctl/container_stats_windows.go
rename to pkg/cmd/container/stats_windows.go
index a6460b3218a..ef2c98fdfad 100644
--- a/cmd/nerdctl/container_stats_windows.go
+++ b/pkg/cmd/container/stats_windows.go
@@ -14,11 +14,11 @@
limitations under the License.
*/
-package main
+package container
import (
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/statsutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/statsutil"
)
func setContainerStatsAndRenderStatsEntry(previousStats *statsutil.ContainerStats, firstSet bool, anydata interface{}, pid int, interfaces []native.NetInterface) (statsutil.StatsEntry, error) {
diff --git a/pkg/cmd/container/stop.go b/pkg/cmd/container/stop.go
index 2a836da7bf0..3000fd611b4 100644
--- a/pkg/cmd/container/stop.go
+++ b/pkg/cmd/container/stop.go
@@ -20,11 +20,12 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Stop stops a list of containers specified by `reqs`.
@@ -35,6 +36,9 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ
if found.MatchCount > 1 {
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
}
+ if err := cleanupNetwork(ctx, found.Container, opt.GOptions); err != nil {
+ return fmt.Errorf("unable to cleanup network for container: %s", found.Req)
+ }
if err := containerutil.Stop(ctx, found.Container, opt.Timeout); err != nil {
if errdefs.IsNotFound(err) {
fmt.Fprintf(opt.Stderr, "No such container: %s\n", found.Req)
@@ -42,7 +46,7 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ
}
return err
}
- _, err := fmt.Fprintf(opt.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(opt.Stdout, found.Req)
return err
},
}
diff --git a/pkg/cmd/container/top.go b/pkg/cmd/container/top.go
index 5973ef11eba..35addf54887 100644
--- a/pkg/cmd/container/top.go
+++ b/pkg/cmd/container/top.go
@@ -28,13 +28,12 @@ package container
import (
"context"
"fmt"
- "regexp"
- "strconv"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// ContainerTopOKBody is from https://github.com/moby/moby/blob/v20.10.6/api/types/container/container_top.go
@@ -61,10 +60,7 @@ func Top(ctx context.Context, client *containerd.Client, containers []string, op
if found.MatchCount > 1 {
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
}
- if err := containerTop(ctx, opt.Stdout, client, found.Container.ID(), strings.Join(containers[1:], " ")); err != nil {
- return err
- }
- return nil
+ return containerTop(ctx, opt.Stdout, client, found.Container.ID(), strings.Join(containers[1:], " "))
},
}
@@ -76,125 +72,3 @@ func Top(ctx context.Context, client *containerd.Client, containers []string, op
}
return nil
}
-
-// appendProcess2ProcList is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L49-L55
-func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) {
- // Make sure number of fields equals number of header titles
- // merging "overhanging" fields
- process := fields[:len(procList.Titles)-1]
- process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
- procList.Processes = append(procList.Processes, process)
-}
-
-// psPidsArg is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L119-L131
-//
-// psPidsArg converts a slice of PIDs to a string consisting
-// of comma-separated list of PIDs prepended by "-q".
-// For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
-func psPidsArg(pids []uint32) string {
- b := []byte{'-', 'q'}
- for i, p := range pids {
- b = strconv.AppendUint(b, uint64(p), 10)
- if i < len(pids)-1 {
- b = append(b, ',')
- }
- }
- return string(b)
-}
-
-// validatePSArgs is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L19-L35
-func validatePSArgs(psArgs string) error {
- // NOTE: \\s does not detect unicode whitespaces.
- // So we use fieldsASCII instead of strings.Fields in parsePSOutput.
- // See https://github.com/docker/docker/pull/24358
- // nolint: gosimple
- re := regexp.MustCompile(`\s+(\S*)=\s*(PID\S*)`)
- for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
- if len(group) >= 3 {
- k := group[1]
- v := group[2]
- if k != "pid" {
- return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
- }
- }
- }
- return nil
-}
-
-// fieldsASCII is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L37-L47
-//
-// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
-func fieldsASCII(s string) []string {
- fn := func(r rune) bool {
- switch r {
- case '\t', '\n', '\f', '\r', ' ':
- return true
- }
- return false
- }
- return strings.FieldsFunc(s, fn)
-}
-
-// hasPid is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L57-L64
-func hasPid(procs []uint32, pid int) bool {
- for _, p := range procs {
- if int(p) == pid {
- return true
- }
- }
- return false
-}
-
-// parsePSOutput is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L66-L117
-func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) {
- procList := &ContainerTopOKBody{}
-
- lines := strings.Split(string(output), "\n")
- procList.Titles = fieldsASCII(lines[0])
-
- pidIndex := -1
- for i, name := range procList.Titles {
- if name == "PID" {
- pidIndex = i
- break
- }
- }
- if pidIndex == -1 {
- return nil, fmt.Errorf("couldn't find PID field in ps output")
- }
-
- // loop through the output and extract the PID from each line
- // fixing #30580, be able to display thread line also when "m" option used
- // in "docker top" client command
- preContainedPidFlag := false
- for _, line := range lines[1:] {
- if len(line) == 0 {
- continue
- }
- fields := fieldsASCII(line)
-
- var (
- p int
- err error
- )
-
- if fields[pidIndex] == "-" {
- if preContainedPidFlag {
- appendProcess2ProcList(procList, fields)
- }
- continue
- }
- p, err = strconv.Atoi(fields[pidIndex])
- if err != nil {
- return nil, fmt.Errorf("unexpected pid '%s': %s", fields[pidIndex], err)
- }
-
- if hasPid(procs, p) {
- preContainedPidFlag = true
- appendProcess2ProcList(procList, fields)
- continue
- }
- preContainedPidFlag = false
- }
- return procList, nil
-}
diff --git a/pkg/cmd/container/top_unix.go b/pkg/cmd/container/top_unix.go
index 6281c23e072..606e6c5772d 100644
--- a/pkg/cmd/container/top_unix.go
+++ b/pkg/cmd/container/top_unix.go
@@ -1,4 +1,4 @@
-//go:build linux || darwin || freebsd || netbsd || openbsd
+//go:build unix
/*
Copyright The containerd Authors.
@@ -34,10 +34,12 @@ import (
"fmt"
"io"
"os/exec"
+ "regexp"
+ "strconv"
"strings"
"text/tabwriter"
- "github.com/containerd/containerd"
+ containerd "github.com/containerd/containerd/v2/client"
)
// containerTop was inspired from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L133-L189
@@ -120,3 +122,125 @@ func containerTop(ctx context.Context, stdio io.Writer, client *containerd.Clien
return w.Flush()
}
+
+// appendProcess2ProcList is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L49-L55
+func appendProcess2ProcList(procList *ContainerTopOKBody, fields []string) {
+ // Make sure number of fields equals number of header titles
+ // merging "overhanging" fields
+ process := fields[:len(procList.Titles)-1]
+ process = append(process, strings.Join(fields[len(procList.Titles)-1:], " "))
+ procList.Processes = append(procList.Processes, process)
+}
+
+// psPidsArg is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L119-L131
+//
+// psPidsArg converts a slice of PIDs to a string consisting
+// of comma-separated list of PIDs prepended by "-q".
+// For example, psPidsArg([]uint32{1,2,3}) returns "-q1,2,3".
+func psPidsArg(pids []uint32) string {
+ b := []byte{'-', 'q'}
+ for i, p := range pids {
+ b = strconv.AppendUint(b, uint64(p), 10)
+ if i < len(pids)-1 {
+ b = append(b, ',')
+ }
+ }
+ return string(b)
+}
+
+// validatePSArgs is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L19-L35
+func validatePSArgs(psArgs string) error {
+ // NOTE: \\s does not detect unicode whitespaces.
+ // So we use fieldsASCII instead of strings.Fields in parsePSOutput.
+ // See https://github.com/docker/docker/pull/24358
+ // nolint: gosimple
+ re := regexp.MustCompile(`\s+(\S*)=\s*(PID\S*)`)
+ for _, group := range re.FindAllStringSubmatch(psArgs, -1) {
+ if len(group) >= 3 {
+ k := group[1]
+ v := group[2]
+ if k != "pid" {
+ return fmt.Errorf("specifying \"%s=%s\" is not allowed", k, v)
+ }
+ }
+ }
+ return nil
+}
+
+// fieldsASCII is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L37-L47
+//
+// fieldsASCII is similar to strings.Fields but only allows ASCII whitespaces
+func fieldsASCII(s string) []string {
+ fn := func(r rune) bool {
+ switch r {
+ case '\t', '\n', '\f', '\r', ' ':
+ return true
+ }
+ return false
+ }
+ return strings.FieldsFunc(s, fn)
+}
+
+// hasPid is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L57-L64
+func hasPid(procs []uint32, pid int) bool {
+ for _, p := range procs {
+ if int(p) == pid {
+ return true
+ }
+ }
+ return false
+}
+
+// parsePSOutput is from https://github.com/moby/moby/blob/v20.10.6/daemon/top_unix.go#L66-L117
+func parsePSOutput(output []byte, procs []uint32) (*ContainerTopOKBody, error) {
+ procList := &ContainerTopOKBody{}
+
+ lines := strings.Split(string(output), "\n")
+ procList.Titles = fieldsASCII(lines[0])
+
+ pidIndex := -1
+ for i, name := range procList.Titles {
+ if name == "PID" {
+ pidIndex = i
+ break
+ }
+ }
+ if pidIndex == -1 {
+ return nil, fmt.Errorf("couldn't find PID field in ps output")
+ }
+
+ // loop through the output and extract the PID from each line
+ // fixing #30580, be able to display thread line also when "m" option used
+ // in "docker top" client command
+ preContainedPidFlag := false
+ for _, line := range lines[1:] {
+ if len(line) == 0 {
+ continue
+ }
+ fields := fieldsASCII(line)
+
+ var (
+ p int
+ err error
+ )
+
+ if fields[pidIndex] == "-" {
+ if preContainedPidFlag {
+ appendProcess2ProcList(procList, fields)
+ }
+ continue
+ }
+ p, err = strconv.Atoi(fields[pidIndex])
+ if err != nil {
+ return nil, fmt.Errorf("unexpected pid '%s': %s", fields[pidIndex], err)
+ }
+
+ if hasPid(procs, p) {
+ preContainedPidFlag = true
+ appendProcess2ProcList(procList, fields)
+ continue
+ }
+ preContainedPidFlag = false
+ }
+ return procList, nil
+}
diff --git a/pkg/cmd/container/top_windows.go b/pkg/cmd/container/top_windows.go
index d5f68a35db9..fcdb165b9b3 100644
--- a/pkg/cmd/container/top_windows.go
+++ b/pkg/cmd/container/top_windows.go
@@ -26,9 +26,10 @@ import (
"time"
"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
- "github.com/containerd/containerd"
- "github.com/containerd/typeurl/v2"
"github.com/docker/go-units"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/typeurl/v2"
)
// containerTop was inspired from https://github.com/moby/moby/blob/master/daemon/top_windows.go
diff --git a/pkg/cmd/container/unpause.go b/pkg/cmd/container/unpause.go
index 7f57dd59038..cc6f8a5781d 100644
--- a/pkg/cmd/container/unpause.go
+++ b/pkg/cmd/container/unpause.go
@@ -20,10 +20,11 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Unpause unpauses all containers specified by `reqs`.
@@ -38,7 +39,7 @@ func Unpause(ctx context.Context, client *containerd.Client, reqs []string, opti
return err
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", found.Req)
+ _, err := fmt.Fprintln(options.Stdout, found.Req)
return err
},
}
diff --git a/pkg/cmd/container/wait.go b/pkg/cmd/container/wait.go
index 4407e825b27..1dbc81d6e44 100644
--- a/pkg/cmd/container/wait.go
+++ b/pkg/cmd/container/wait.go
@@ -18,13 +18,14 @@ package container
import (
"context"
+ "errors"
"fmt"
"io"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/hashicorp/go-multierror"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
)
// Wait blocks until all the containers specified by reqs have stopped, then print their exit codes.
@@ -46,14 +47,14 @@ func Wait(ctx context.Context, client *containerd.Client, reqs []string, options
return err
}
- var allErr error
+ var errs []error
w := options.Stdout
for _, container := range containers {
if waitErr := waitContainer(ctx, w, container); waitErr != nil {
- allErr = multierror.Append(allErr, waitErr)
+ errs = append(errs, waitErr)
}
}
- return allErr
+ return errors.Join(errs...)
}
func waitContainer(ctx context.Context, w io.Writer, container containerd.Container) error {
diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go
index 75d8d98a958..71a2cca4c54 100644
--- a/pkg/cmd/image/convert.go
+++ b/pkg/cmd/image/convert.go
@@ -25,26 +25,28 @@ import (
"os"
"strings"
+ "github.com/klauspost/compress/zstd"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
overlaybdconvert "github.com/containerd/accelerated-container-image/pkg/convertor"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/images/converter"
- "github.com/containerd/containerd/images/converter/uncompress"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- converterutil "github.com/containerd/nerdctl/pkg/imgutil/converter"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/converter"
+ "github.com/containerd/containerd/v2/core/images/converter/uncompress"
+ "github.com/containerd/log"
nydusconvert "github.com/containerd/nydus-snapshotter/pkg/converter"
"github.com/containerd/stargz-snapshotter/estargz"
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
estargzexternaltocconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz/externaltoc"
zstdchunkedconvert "github.com/containerd/stargz-snapshotter/nativeconverter/zstdchunked"
"github.com/containerd/stargz-snapshotter/recorder"
- "github.com/klauspost/compress/zstd"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ converterutil "github.com/containerd/nerdctl/v2/pkg/imgutil/converter"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRawRef string, options types.ImageConvertOptions) error {
@@ -55,17 +57,17 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
return errors.New("src and target image need to be specified")
}
- srcNamed, err := referenceutil.ParseAny(srcRawRef)
+ parsedReference, err := referenceutil.Parse(srcRawRef)
if err != nil {
return err
}
- srcRef := srcNamed.String()
+ srcRef := parsedReference.String()
- targetNamed, err := referenceutil.ParseDockerRef(targetRawRef)
+ parsedReference, err = referenceutil.Parse(targetRawRef)
if err != nil {
return err
}
- targetRef := targetNamed.String()
+ targetRef := parsedReference.String()
platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platforms)
if err != nil {
@@ -73,16 +75,26 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
}
convertOpts = append(convertOpts, converter.WithPlatform(platMC))
+ // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425
+ err = EnsureAllContent(ctx, client, srcRef, options.GOptions)
+ if err != nil {
+ return err
+ }
+
estargz := options.Estargz
+ zstd := options.Zstd
zstdchunked := options.ZstdChunked
overlaybd := options.Overlaybd
nydus := options.Nydus
var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
- if estargz || zstdchunked || overlaybd || nydus {
+ if estargz || zstd || zstdchunked || overlaybd || nydus {
convertCount := 0
if estargz {
convertCount++
}
+ if zstd {
+ convertCount++
+ }
if zstdchunked {
convertCount++
}
@@ -106,6 +118,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
return err
}
convertType = "estargz"
+ case zstd:
+ convertFunc, err = getZstdConverter(options)
+ if err != nil {
+ return err
+ }
+ convertType = "zstd"
case zstdchunked:
convertFunc, err = getZstdchunkedConverter(options)
if err != nil {
@@ -153,9 +171,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
}
if !options.Oci {
if nydus || overlaybd {
- logrus.Warnf("option --%s should be used in conjunction with --oci, forcibly enabling on oci mediatype for %s conversion", convertType, convertType)
+ log.G(ctx).Warnf("option --%s should be used in conjunction with --oci, forcibly enabling on oci mediatype for %s conversion", convertType, convertType)
} else {
- logrus.Warnf("option --%s should be used in conjunction with --oci", convertType)
+ log.G(ctx).Warnf("option --%s should be used in conjunction with --oci", convertType)
}
}
if options.Uncompress {
@@ -244,7 +262,7 @@ func getESGZConvertOpts(options types.ImageConvertOptions) ([]estargz.Option, er
return nil, fmt.Errorf("estargz-record-in requires experimental mode to be enabled")
}
- logrus.Warn("--estargz-record-in flag is experimental and subject to change")
+ log.L.Warn("--estargz-record-in flag is experimental and subject to change")
paths, err := readPathsFromRecordFile(options.EstargzRecordIn)
if err != nil {
return nil, err
@@ -256,6 +274,10 @@ func getESGZConvertOpts(options types.ImageConvertOptions) ([]estargz.Option, er
return esgzOpts, nil
}
+func getZstdConverter(options types.ImageConvertOptions) (converter.ConvertFunc, error) {
+ return converterutil.ZstdLayerConvertFunc(options)
+}
+
func getZstdchunkedConverter(options types.ImageConvertOptions) (converter.ConvertFunc, error) {
esgzOpts := []estargz.Option{
@@ -267,7 +289,7 @@ func getZstdchunkedConverter(options types.ImageConvertOptions) (converter.Conve
return nil, fmt.Errorf("zstdchunked-record-in requires experimental mode to be enabled")
}
- logrus.Warn("--zstdchunked-record-in flag is experimental and subject to change")
+ log.L.Warn("--zstdchunked-record-in flag is experimental and subject to change")
paths, err := readPathsFromRecordFile(options.ZstdChunkedRecordIn)
if err != nil {
return nil, err
@@ -342,14 +364,14 @@ func printConvertedImage(stdout io.Writer, options types.ImageConvertOptions, im
for i, e := range img.ExtraImages {
elems := strings.SplitN(e, "@", 2)
if len(elems) < 2 {
- logrus.Errorf("extra reference %q doesn't contain digest", e)
+ log.L.Errorf("extra reference %q doesn't contain digest", e)
} else {
- logrus.Infof("Extra image(%d) %s", i, elems[0])
+ log.L.Infof("Extra image(%d) %s", i, elems[0])
}
}
elems := strings.SplitN(img.Image, "@", 2)
if len(elems) < 2 {
- logrus.Errorf("reference %q doesn't contain digest", img.Image)
+ log.L.Errorf("reference %q doesn't contain digest", img.Image)
} else {
fmt.Fprintln(stdout, elems[1])
}
diff --git a/pkg/cmd/image/crypt.go b/pkg/cmd/image/crypt.go
index 9aa08318e1c..981d39dfd51 100644
--- a/pkg/cmd/image/crypt.go
+++ b/pkg/cmd/image/crypt.go
@@ -21,15 +21,17 @@ import (
"errors"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images/converter"
- "github.com/containerd/imgcrypt/images/encryption"
- "github.com/containerd/imgcrypt/images/encryption/parsehelpers"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images/converter"
+ "github.com/containerd/imgcrypt/v2/images/encryption"
+ "github.com/containerd/imgcrypt/v2/images/encryption/parsehelpers"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
func Crypt(ctx context.Context, client *containerd.Client, srcRawRef, targetRawRef string, encrypt bool, options types.ImageCryptOptions) error {
@@ -38,17 +40,17 @@ func Crypt(ctx context.Context, client *containerd.Client, srcRawRef, targetRawR
return errors.New("src and target image need to be specified")
}
- srcNamed, err := referenceutil.ParseAny(srcRawRef)
+ parsedRerefence, err := referenceutil.Parse(srcRawRef)
if err != nil {
return err
}
- srcRef := srcNamed.String()
+ srcRef := parsedRerefence.String()
- targetNamed, err := referenceutil.ParseDockerRef(targetRawRef)
+ parsedRerefence, err = referenceutil.Parse(targetRawRef)
if err != nil {
return err
}
- targetRef := targetNamed.String()
+ targetRef := parsedRerefence.String()
platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platforms)
if err != nil {
diff --git a/pkg/cmd/image/ensure.go b/pkg/cmd/image/ensure.go
new file mode 100644
index 00000000000..fe5f6ca88c5
--- /dev/null
+++ b/pkg/cmd/image/ensure.go
@@ -0,0 +1,120 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package image
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "os"
+
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerdutil"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/fetch"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+)
+
+func EnsureAllContent(ctx context.Context, client *containerd.Client, srcName string, options types.GlobalCommandOptions) error {
+ // Get the image from the srcName
+ imageService := client.ImageService()
+ img, err := imageService.Get(ctx, srcName)
+ if err != nil {
+ return err
+ }
+
+ provider := containerdutil.NewProvider(client)
+ snapshotter := containerdutil.SnapshotService(client, options.Snapshotter)
+ // Read the image
+ imagesList, _ := read(ctx, provider, snapshotter, img.Target)
+ // Iterate through the list
+ for _, i := range imagesList {
+ err = ensureOne(ctx, client, srcName, img.Target, i.platform, options)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func ensureOne(ctx context.Context, client *containerd.Client, rawRef string, target ocispec.Descriptor, platform ocispec.Platform, options types.GlobalCommandOptions) error {
+ parsedReference, err := referenceutil.Parse(rawRef)
+ if err != nil {
+ return err
+ }
+ pltf := []ocispec.Platform{platform}
+ platformComparer := platformutil.NewMatchComparerFromOCISpecPlatformSlice(pltf)
+
+ _, _, _, missing, err := images.Check(ctx, client.ContentStore(), target, platformComparer)
+ if err != nil {
+ return err
+ }
+
+ if len(missing) > 0 {
+ // Get a resolver
+ var dOpts []dockerconfigresolver.Opt
+ if options.InsecureRegistry {
+ log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain)
+ dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
+ }
+ dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.HostsDir))
+ resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...)
+ if err != nil {
+ return err
+ }
+ config := &fetch.Config{
+ Resolver: resolver,
+ RemoteOpts: []containerd.RemoteOpt{},
+ Platforms: pltf,
+ ProgressOutput: os.Stderr,
+ }
+
+ err = fetch.Fetch(ctx, client, rawRef, config)
+
+ if err != nil {
+ // In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused".
+ if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) {
+ return err
+ }
+ if options.InsecureRegistry {
+ log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain)
+ dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
+ resolver, err = dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...)
+ if err != nil {
+ return err
+ }
+ config.Resolver = resolver
+ return fetch.Fetch(ctx, client, rawRef, config)
+ }
+ log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", parsedReference.Domain)
+ log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
+ }
+
+ return err
+ }
+
+ return nil
+}
diff --git a/pkg/cmd/image/inspect.go b/pkg/cmd/image/inspect.go
index 16cd258e02a..ec96ddb7cba 100644
--- a/pkg/cmd/image/inspect.go
+++ b/pkg/cmd/image/inspect.go
@@ -18,59 +18,190 @@ package image
import (
"context"
+ "errors"
"fmt"
+ "regexp"
+ "strings"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/imageinspector"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerdutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/imageinspector"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
+func inspectIdentifier(ctx context.Context, client *containerd.Client, identifier string) ([]images.Image, string, string, error) {
+ // Figure out what we have here - digest, tag, name
+ parsedReference, err := referenceutil.Parse(identifier)
+ if err != nil {
+ return nil, "", "", err
+ }
+ digest := ""
+ if parsedReference.Digest != "" {
+ digest = parsedReference.Digest.String()
+ }
+ name := parsedReference.Name()
+ tag := parsedReference.Tag
+
+ // Initialize filters
+ var filters []string
+ // This will hold the final image list, if any
+ var imageList []images.Image
+
+ // No digest in the request? Then assume it is a name
+ if digest == "" {
+ filters = []string{fmt.Sprintf("name==%s:%s", name, tag)}
+ // Query it
+ imageList, err = client.ImageService().List(ctx, filters...)
+ if err != nil {
+ return nil, "", "", fmt.Errorf("containerd image service failed: %w", err)
+ }
+ // Nothing? Then it could be a short id (aka truncated digest) - we are going to use this
+ if len(imageList) == 0 {
+ digest = fmt.Sprintf("sha256:%s.*", regexp.QuoteMeta(strings.TrimPrefix(identifier, "sha256:")))
+ name = ""
+ tag = ""
+ } else {
+ // Otherwise, we found one by name. Get the digest from it.
+ digest = imageList[0].Target.Digest.String()
+ }
+ }
+
+ // At this point, we DO have a digest (or short id), so, that is what we are retrieving
+ filters = []string{fmt.Sprintf("target.digest~=^%s$", digest)}
+ imageList, err = client.ImageService().List(ctx, filters...)
+ if err != nil {
+ return nil, "", "", fmt.Errorf("containerd image service failed: %w", err)
+ }
+
+ // TODO: docker does allow retrieving images by Id, so implement as a last ditch effort (probably look-up the store)
+
+ // Return the list we found, along with normalized name and tag
+ return imageList, name, tag, nil
+}
+
// Inspect prints detailed information of each image in `images`.
-func Inspect(ctx context.Context, client *containerd.Client, images []string, options types.ImageInspectOptions) error {
- f := &imageInspector{
- mode: options.Mode,
+func Inspect(ctx context.Context, client *containerd.Client, identifiers []string, options types.ImageInspectOptions) error {
+ // Verify we have a valid mode
+ // TODO: move this out of here, to Cobra command line arg validation
+ if options.Mode != "native" && options.Mode != "dockercompat" {
+ return fmt.Errorf("unknown mode %q", options.Mode)
}
- walker := &imagewalker.ImageWalker{
- Client: client,
- OnFound: func(ctx context.Context, found imagewalker.Found) error {
- ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
- defer cancel()
+ // Set a timeout
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
+
+ // Will hold the final answers
+ var errs []error
+ var entries []interface{}
- n, err := imageinspector.Inspect(ctx, client, found.Image, options.GOptions.Snapshotter)
+ snapshotter := containerdutil.SnapshotService(client, options.GOptions.Snapshotter)
+ // We have to query per provided identifier, as we need to post-process results for the case name + digest
+ for _, identifier := range identifiers {
+ candidateImageList, requestedName, requestedTag, err := inspectIdentifier(ctx, client, identifier)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("%w: %s", err, identifier))
+ continue
+ }
+
+ var validatedImage *dockercompat.Image
+ var repoTags []string
+ var repoDigests []string
+
+ // Go through the candidates
+ for _, candidateImage := range candidateImageList {
+ // Inspect the image
+ candidateNativeImage, err := imageinspector.Inspect(ctx, client, candidateImage, snapshotter)
if err != nil {
- return err
+ log.G(ctx).WithError(err).WithField("name", candidateImage.Name).Error("failure inspecting image")
+ continue
}
- switch f.mode {
- case "native":
- f.entries = append(f.entries, n)
- case "dockercompat":
- d, err := dockercompat.ImageFromNative(n)
+
+ // If native, we just add everything in there and that's it
+ if options.Mode == "native" {
+ entries = append(entries, candidateNativeImage)
+ continue
+ }
+
+ // If dockercompat: does the candidate have a name? Get it if so
+ parsedReference, err := referenceutil.Parse(candidateNativeImage.Image.Name)
+ if err != nil {
+ log.G(ctx).WithError(err).WithField("name", candidateNativeImage.Image.Name).Error("the found image has an unparsable name")
+ continue
+ }
+
+ // If we were ALSO asked for a specific name on top of the digest, we need to make sure we keep only the image with that name
+ if requestedName != "" {
+ // If the candidate did not have a name, then we should ignore this one and continue
+ if parsedReference.Name() == "" {
+ continue
+ }
+
+ // Otherwise, the candidate has a name. If it is the one we want, store it and continue, otherwise, fall through
+ candidateTag := parsedReference.Tag
+ // If the name had a digest, an empty tag is not normalized to latest, so, account for that here
+ if requestedTag == "" {
+ requestedTag = "latest"
+ }
+ if parsedReference.Name() == requestedName && candidateTag == requestedTag {
+ validatedImage, err = dockercompat.ImageFromNative(candidateNativeImage)
+ if err != nil {
+ log.G(ctx).WithError(err).WithField("name", candidateNativeImage.Image.Name).Error("could not get a docker compat version of the native image")
+ }
+ continue
+ }
+ } else if validatedImage == nil {
+ // Alternatively, we got a request by digest only, so, if we do not know about it already, store it and continue
+ validatedImage, err = dockercompat.ImageFromNative(candidateNativeImage)
if err != nil {
- return err
+ log.G(ctx).WithError(err).WithField("name", candidateNativeImage.Image.Name).Error("could not get a docker compat version of the native image")
}
- f.entries = append(f.entries, d)
- default:
- return fmt.Errorf("unknown mode %q", f.mode)
+ continue
}
- return nil
- },
+
+ // Fallthrough cases:
+ // - we got a request by digest, but we already had the image stored
+ // - we got a request by name, and the name of the candidate did not match the requested name
+ // Now, check if the candidate has a name - if it does, populate repoTags and repoDigests
+ if parsedReference.Name() != "" {
+ tag := parsedReference.Tag
+ if tag == "" {
+ tag = "latest"
+ }
+ repoTags = append(repoTags, fmt.Sprintf("%s:%s", parsedReference.FamiliarName(), tag))
+ repoDigests = append(repoDigests, fmt.Sprintf("%s@%s", parsedReference.FamiliarName(), candidateImage.Target.Digest.String()))
+ }
+ }
+
+ // Done iterating through candidates. Did we find anything that matches?
+ if validatedImage != nil {
+ // Then slap in the repoTags and repoDigests we found from the other candidates
+ validatedImage.RepoTags = append(validatedImage.RepoTags, repoTags...)
+ validatedImage.RepoDigests = append(validatedImage.RepoDigests, repoDigests...)
+ // Store our image
+ // foundImages[validatedDigest] = validatedImage
+ entries = append(entries, validatedImage)
+ } else {
+ errs = append(errs, fmt.Errorf("no such image: %s", identifier))
+ }
}
- err := walker.WalkAll(ctx, images, true)
- if len(f.entries) > 0 {
- if formatErr := formatter.FormatSlice(options.Format, options.Stdout, f.entries); formatErr != nil {
- logrus.Error(formatErr)
+ // Display
+ if len(entries) > 0 {
+ if formatErr := formatter.FormatSlice(options.Format, options.Stdout, entries); formatErr != nil {
+ log.G(ctx).Error(formatErr)
}
}
- return err
-}
-type imageInspector struct {
- mode string
- entries []interface{}
+ if len(errs) > 0 {
+ return fmt.Errorf("%d errors:\n%w", len(errs), errors.Join(errs...))
+ }
+
+ return nil
}
diff --git a/pkg/cmd/image/list.go b/pkg/cmd/image/list.go
index 247757b2374..c7440b459fb 100644
--- a/pkg/cmd/image/list.go
+++ b/pkg/cmd/image/list.go
@@ -19,29 +19,37 @@ package image
import (
"bytes"
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
+ "sort"
"strings"
"text/tabwriter"
"text/template"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/containerd/snapshots"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/imgutil"
- v1 "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+ "github.com/docker/go-units"
+ "github.com/opencontainers/go-digest"
+ "github.com/opencontainers/image-spec/identity"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerdutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
// ListCommandHandler `List` and print images matching filters in `options`.
-func ListCommandHandler(ctx context.Context, client *containerd.Client, options types.ImageListOptions) error {
+func ListCommandHandler(ctx context.Context, client *containerd.Client, options *types.ImageListOptions) error {
imageList, err := List(ctx, client, options.Filters, options.NameAndRefFilter)
if err != nil {
return err
@@ -74,37 +82,34 @@ func List(ctx context.Context, client *containerd.Client, filters, nameAndRefFil
return nil, err
}
- if f.Dangling != nil {
- imageList = imgutil.FilterDangling(imageList, *f.Dangling)
+ filters := []imgutil.Filter{}
+ if f.Dangling != nil && *f.Dangling {
+ filters = append(filters, imgutil.FilterDanglingImages())
+ } else if f.Dangling != nil {
+ filters = append(filters, imgutil.FilterTaggedImages())
}
- imageList, err = imgutil.FilterByLabel(ctx, client, imageList, f.Labels)
- if err != nil {
- return nil, err
+ if len(f.Labels) > 0 {
+ filters = append(filters, imgutil.FilterByLabel(ctx, client, f.Labels))
}
- imageList, err = imgutil.FilterByReference(imageList, f.Reference)
- if err != nil {
- return nil, err
+ if len(f.Reference) > 0 {
+ filters = append(filters, imgutil.FilterByReference(f.Reference))
}
- var beforeImages []images.Image
- if len(f.Before) > 0 {
- beforeImages, err = imageStore.List(ctx, f.Before...)
- if err != nil {
- return nil, err
- }
- }
- var sinceImages []images.Image
- if len(f.Since) > 0 {
- sinceImages, err = imageStore.List(ctx, f.Since...)
- if err != nil {
- return nil, err
- }
+ if len(f.Before) > 0 || len(f.Since) > 0 {
+ filters = append(filters, imgutil.FilterByCreatedAt(ctx, client, f.Before, f.Since))
}
- imageList = imgutil.FilterImages(imageList, beforeImages, sinceImages)
+ imageList, err = imgutil.ApplyFilters(imageList, filters...)
+ if err != nil {
+ return []images.Image{}, err
+ }
}
+
+ sort.Slice(imageList, func(i, j int) bool {
+ return imageList[i].CreatedAt.After(imageList[j].CreatedAt)
+ })
return imageList, nil
}
@@ -123,8 +128,48 @@ type imagePrintable struct {
Platform string // nerdctl extension
}
-func printImages(ctx context.Context, client *containerd.Client, imageList []images.Image, options types.ImageListOptions) error {
+func printImages(ctx context.Context, client *containerd.Client, imageList []images.Image, options *types.ImageListOptions) error {
w := options.Stdout
+ var finalImageList []images.Image
+ /*
+ the same imageId under k8s.io is showing multiple results: repo:tag, repo:digest, configID.
+ We expect to display only repo:tag, consistent with other namespaces and CRI
+ e.g.
+ nerdctl -n k8s.io images
+ REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
+ centos 7 be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ centos be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ expect:
+ nerdctl --kube-hide-dupe -n k8s.io images
+ REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
+ centos 7 be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ */
+ if options.GOptions.KubeHideDupe && options.GOptions.Namespace == "k8s.io" {
+ imageDigest := make(map[digest.Digest]bool)
+ var imageNoTag []images.Image
+ for _, img := range imageList {
+ parsed, err := referenceutil.Parse(img.Name)
+ if err != nil {
+ continue
+ }
+ if parsed.Tag != "" {
+ finalImageList = append(finalImageList, img)
+ imageDigest[img.Target.Digest] = true
+ continue
+ }
+ imageNoTag = append(imageNoTag, img)
+ }
+ //Ensure that dangling images without a repo:tag are displayed correctly.
+ for _, ima := range imageNoTag {
+ if !imageDigest[ima.Target.Digest] {
+ finalImageList = append(finalImageList, ima)
+ imageDigest[ima.Target.Digest] = true
+ }
+ }
+ } else {
+ finalImageList = imageList
+ }
digestsFlag := options.Digests
if options.Format == "wide" {
digestsFlag = true
@@ -160,20 +205,20 @@ func printImages(ctx context.Context, client *containerd.Client, imageList []ima
}
printer := &imagePrinter{
- w: w,
- quiet: options.Quiet,
- noTrunc: options.NoTrunc,
- digestsFlag: digestsFlag,
- namesFlag: options.Names,
- tmpl: tmpl,
- client: client,
- contentStore: client.ContentStore(),
- snapshotter: client.SnapshotService(options.GOptions.Snapshotter),
+ w: w,
+ quiet: options.Quiet,
+ noTrunc: options.NoTrunc,
+ digestsFlag: digestsFlag,
+ namesFlag: options.Names,
+ tmpl: tmpl,
+ client: client,
+ provider: containerdutil.NewProvider(client),
+ snapshotter: containerdutil.SnapshotService(client, options.GOptions.Snapshotter),
}
- for _, img := range imageList {
+ for _, img := range finalImageList {
if err := printer.printImage(ctx, img); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}
if f, ok := w.(formatter.Flusher); ok {
@@ -187,36 +232,127 @@ type imagePrinter struct {
quiet, noTrunc, digestsFlag, namesFlag bool
tmpl *template.Template
client *containerd.Client
- contentStore content.Store
+ provider content.Provider
snapshotter snapshots.Snapshotter
}
-func (x *imagePrinter) printImage(ctx context.Context, img images.Image) error {
- ociPlatforms, err := images.Platforms(ctx, x.contentStore, img.Target)
+type image struct {
+ blobSize int64
+ size int64
+ platform platforms.Platform
+ config *ocispec.Descriptor
+}
+
+func readManifest(ctx context.Context, provider content.Provider, snapshotter snapshots.Snapshotter, desc ocispec.Descriptor) (*image, error) {
+ // Read the manifest blob from the descriptor
+ manifestData, err := containerdutil.ReadBlob(ctx, provider, desc)
if err != nil {
- logrus.WithError(err).Warnf("failed to get the platform list of image %q", img.Name)
- return x.printImageSinglePlatform(ctx, img, platforms.DefaultSpec())
+ return nil, err
+ }
+
+ // Unmarshal as Manifest
+ var manifest ocispec.Manifest
+ if err := json.Unmarshal(manifestData, &manifest); err != nil {
+ return nil, err
}
- for _, ociPlatform := range ociPlatforms {
- if err := x.printImageSinglePlatform(ctx, img, ociPlatform); err != nil {
- logrus.WithError(err).Warnf("failed to get platform %q of image %q", platforms.Format(ociPlatform), img.Name)
+
+ // Now, read the config
+ configData, err := containerdutil.ReadBlob(ctx, provider, manifest.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ // Unmarshal as Image
+ var config ocispec.Image
+ if err := json.Unmarshal(configData, &config); err != nil {
+ log.G(ctx).Error("Error unmarshaling config")
+ return nil, err
+ }
+
+ // If we are here, the image exists and is valid, so, do our size lookups
+
+ // Aggregate the descriptor size, and blob size from the config and layers
+ blobSize := desc.Size + manifest.Config.Size
+ for _, layerDescriptor := range manifest.Layers {
+ blobSize += layerDescriptor.Size
+ }
+
+ // Get the platform
+ plt := platforms.Normalize(ocispec.Platform{OS: config.OS, Architecture: config.Architecture, Variant: config.Variant})
+
+ // Get the filesystem size for all layers
+ chainID := identity.ChainID(config.RootFS.DiffIDs).String()
+ size := int64(0)
+ if _, actualSize, err := imgutil.ResourceUsage(ctx, snapshotter, chainID); err == nil {
+ size = actualSize.Size
+ }
+
+ return &image{
+ blobSize: blobSize,
+ size: size,
+ platform: plt,
+ config: &manifest.Config,
+ }, nil
+}
+
+func readIndex(ctx context.Context, provider content.Provider, snapshotter snapshots.Snapshotter, desc ocispec.Descriptor) (map[string]*image, error) {
+ descs := map[string]*image{}
+
+ // Read the index
+ indexData, err := containerdutil.ReadBlob(ctx, provider, desc)
+ if err != nil {
+ return nil, err
+ }
+
+ // Unmarshal as Index
+ var index ocispec.Index
+ if err := json.Unmarshal(indexData, &index); err != nil {
+ return nil, err
+ }
+
+ // Iterate over manifest descriptors and read them all
+ for _, manifestDescriptor := range index.Manifests {
+ manifest, err := readManifest(ctx, provider, snapshotter, manifestDescriptor)
+ if err != nil {
+ continue
}
+ descs[platforms.FormatAll(manifest.platform)] = manifest
}
- return nil
+ return descs, err
}
-func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images.Image, ociPlatform v1.Platform) error {
- platMC := platforms.OnlyStrict(ociPlatform)
- if avail, _, _, _, availErr := images.Check(ctx, x.contentStore, img.Target, platMC); !avail {
- logrus.WithError(availErr).Debugf("skipping printing image %q for platform %q", img.Name, platforms.Format(ociPlatform))
- return nil
+func read(ctx context.Context, provider content.Provider, snapshotter snapshots.Snapshotter, desc ocispec.Descriptor) (map[string]*image, error) {
+ if images.IsManifestType(desc.MediaType) {
+ manifest, err := readManifest(ctx, provider, snapshotter, desc)
+ if err != nil {
+ return nil, err
+ }
+ descs := map[string]*image{}
+ descs[platforms.FormatAll(manifest.platform)] = manifest
+ return descs, nil
+ }
+ if images.IsIndexType(desc.MediaType) {
+ return readIndex(ctx, provider, snapshotter, desc)
}
+ return nil, fmt.Errorf("unknown media type: %s", desc.MediaType)
+}
- image := containerd.NewImageWithPlatform(x.client, img, platMC)
- desc, err := image.Config(ctx)
+func (x *imagePrinter) printImage(ctx context.Context, img images.Image) error {
+ candidateImages, err := read(ctx, x.provider, x.snapshotter, img.Target)
if err != nil {
- logrus.WithError(err).Warnf("failed to get config of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
+ return err
}
+
+ for platform, desc := range candidateImages {
+ if err := x.printImageSinglePlatform(*desc.config, img, desc.blobSize, desc.size, desc.platform); err != nil {
+ log.G(ctx).WithError(err).Debugf("failed to get platform %q of image %q", platform, img.Name)
+ }
+ }
+
+ return nil
+}
+
+func (x *imagePrinter) printImageSinglePlatform(desc ocispec.Descriptor, img images.Image, blobSize int64, size int64, plt platforms.Platform) error {
var (
repository string
tag string
@@ -226,17 +362,6 @@ func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images.
repository, tag = imgutil.ParseRepoTag(img.Name)
}
- blobSize, err := image.Size(ctx)
- if err != nil {
- logrus.WithError(err).Warnf("failed to get blob size of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
- }
-
- size, err := imgutil.UnpackedImageSize(ctx, x.snapshotter, image)
- if err != nil {
- // Warnf is too verbose: https://github.com/containerd/nerdctl/issues/2058
- logrus.WithError(err).Debugf("failed to get unpacked size of image %q for platform %q", img.Name, platforms.Format(ociPlatform))
- }
-
p := imagePrintable{
CreatedAt: img.CreatedAt.Round(time.Second).Local().String(), // format like "2021-08-07 02:19:45 +0900 JST"
CreatedSince: formatter.TimeSinceInHuman(img.CreatedAt),
@@ -245,9 +370,9 @@ func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images.
Repository: repository,
Tag: tag,
Name: img.Name,
- Size: progress.Bytes(size).String(),
- BlobSize: progress.Bytes(blobSize).String(),
- Platform: platforms.Format(ociPlatform),
+ Size: units.HumanSize(float64(size)),
+ BlobSize: units.HumanSize(float64(blobSize)),
+ Platform: platforms.FormatAll(plt),
}
if p.Repository == "" {
p.Repository = ""
@@ -264,11 +389,11 @@ func (x *imagePrinter) printImageSinglePlatform(ctx context.Context, img images.
if err := x.tmpl.Execute(&b, p); err != nil {
return err
}
- if _, err = fmt.Fprintf(x.w, b.String()+"\n"); err != nil {
+ if _, err := fmt.Fprintln(x.w, b.String()); err != nil {
return err
}
} else if x.quiet {
- if _, err := fmt.Fprintf(x.w, "%s\n", p.ID); err != nil {
+ if _, err := fmt.Fprintln(x.w, p.ID); err != nil {
return err
}
} else {
diff --git a/pkg/cmd/image/load.go b/pkg/cmd/image/load.go
deleted file mode 100644
index b3871279dd5..00000000000
--- a/pkg/cmd/image/load.go
+++ /dev/null
@@ -1,113 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package image
-
-import (
- "context"
- "errors"
- "fmt"
- "io"
- "os"
-
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/archive/compression"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/images/archive"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/platformutil"
-)
-
-type readCounter struct {
- io.Reader
- N int
-}
-
-func (r *readCounter) Read(p []byte) (int, error) {
- n, err := r.Reader.Read(p)
- if n > 0 {
- r.N += n
- }
- return n, err
-}
-
-func Load(ctx context.Context, client *containerd.Client, options types.ImageLoadOptions) error {
- if options.Input != "" {
- f, err := os.Open(options.Input)
- if err != nil {
- return err
- }
- defer f.Close()
- options.Stdin = f
- } else {
- // check if stdin is empty.
- stdinStat, err := os.Stdin.Stat()
- if err != nil {
- return err
- }
- if stdinStat.Size() == 0 && (stdinStat.Mode()&os.ModeNamedPipe) == 0 {
- return errors.New("stdin is empty and input flag is not specified")
- }
- }
- decompressor, err := compression.DecompressStream(options.Stdin)
- if err != nil {
- return err
- }
- platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platform)
- if err != nil {
- return err
- }
- return loadImage(ctx, client, decompressor, platMC, false, options)
-}
-
-func loadImage(ctx context.Context, client *containerd.Client, in io.Reader, platMC platforms.MatchComparer, quiet bool, options types.ImageLoadOptions) error {
- // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient().
- // Otherwise unpacking may fail.
- r := &readCounter{Reader: in}
- imgs, err := client.Import(ctx, r, containerd.WithDigestRef(archive.DigestTranslator(options.GOptions.Snapshotter)), containerd.WithSkipDigestRef(func(name string) bool { return name != "" }), containerd.WithImportPlatform(platMC))
- if err != nil {
- if r.N == 0 {
- // Avoid confusing "unrecognized image format"
- return errors.New("no image was built")
- }
- if errors.Is(err, images.ErrEmptyWalk) {
- err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err)
- }
- return err
- }
- for _, img := range imgs {
- image := containerd.NewImageWithPlatform(client, img, platMC)
-
- // TODO: Show unpack status
- if !quiet {
- fmt.Fprintf(options.Stdout, "unpacking %s (%s)...\n", img.Name, img.Target.Digest)
- }
- err = image.Unpack(ctx, options.GOptions.Snapshotter)
- if err != nil {
- return err
- }
- if quiet {
- fmt.Fprintln(options.Stdout, img.Target.Digest)
- } else {
- repo, tag := imgutil.ParseRepoTag(img.Name)
- fmt.Fprintf(options.Stdout, "Loaded image: %s:%s\n", repo, tag)
- }
- }
-
- return nil
-}
diff --git a/pkg/cmd/image/prune.go b/pkg/cmd/image/prune.go
index 32733153508..da29fbdb486 100644
--- a/pkg/cmd/image/prune.go
+++ b/pkg/cmd/image/prune.go
@@ -20,60 +20,63 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/imgutil"
"github.com/opencontainers/go-digest"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
)
// Prune will remove all dangling images. If all is specified, will also remove all images not referenced by any container.
func Prune(ctx context.Context, client *containerd.Client, options types.ImagePruneOptions) error {
var (
- imageStore = client.ImageService()
- contentStore = client.ContentStore()
- containerStore = client.ContainerService()
+ imageStore = client.ImageService()
+ contentStore = client.ContentStore()
)
- imageList, err := imageStore.List(ctx)
- if err != nil {
- return err
- }
-
- var filteredImages []images.Image
+ var (
+ imagesToBeRemoved []images.Image
+ err error
+ )
- if options.All {
- containerList, err := containerStore.List(ctx)
+ filters := []imgutil.Filter{}
+ if len(options.Filters) > 0 {
+ parsedFilters, err := imgutil.ParseFilters(options.Filters)
if err != nil {
return err
}
- usedImages := make(map[string]struct{})
- for _, container := range containerList {
- usedImages[container.Image] = struct{}{}
+ if len(parsedFilters.Labels) > 0 {
+ filters = append(filters, imgutil.FilterByLabel(ctx, client, parsedFilters.Labels))
}
-
- for _, image := range imageList {
- if _, ok := usedImages[image.Name]; ok {
- continue
- }
-
- filteredImages = append(filteredImages, image)
+ if len(parsedFilters.Until) > 0 {
+ filters = append(filters, imgutil.FilterUntil(parsedFilters.Until))
}
+ }
+
+ if options.All {
+ // Remove all unused images; not just dangling ones
+ imagesToBeRemoved, err = imgutil.GetUnusedImages(ctx, client, filters...)
} else {
- filteredImages = imgutil.FilterDangling(imageList, true)
+ // Remove dangling images only
+ imagesToBeRemoved, err = imgutil.GetDanglingImages(ctx, client, filters...)
+ }
+ if err != nil {
+ return err
}
delOpts := []images.DeleteOpt{images.SynchronousDelete()}
removedImages := make(map[string][]digest.Digest)
- for _, image := range filteredImages {
+ for _, image := range imagesToBeRemoved {
digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict())
if err != nil {
- logrus.WithError(err).Warnf("failed to enumerate rootfs")
+ log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs")
}
if err := imageStore.Delete(ctx, image.Name, delOpts...); err != nil {
- logrus.WithError(err).Warnf("failed to delete image %s", image.Name)
+ log.G(ctx).WithError(err).Warnf("failed to delete image %s", image.Name)
continue
}
removedImages[image.Name] = digests
diff --git a/pkg/cmd/image/pull.go b/pkg/cmd/image/pull.go
index 50ac66bb97f..1d943c9b62d 100644
--- a/pkg/cmd/image/pull.go
+++ b/pkg/cmd/image/pull.go
@@ -22,30 +22,18 @@ import (
"os"
"path/filepath"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/ipfs"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/containerd/nerdctl/pkg/signutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- v1 "github.com/opencontainers/image-spec/specs-go/v1"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/ipfs"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+ "github.com/containerd/nerdctl/v2/pkg/signutil"
)
// Pull pulls an image specified by `rawRef`.
func Pull(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePullOptions) error {
- ocispecPlatforms, err := platformutil.NewOCISpecPlatformSlice(options.AllPlatforms, options.Platform)
- if err != nil {
- return err
- }
-
- unpack, err := strutil.ParseBoolOrAuto(options.Unpack)
- if err != nil {
- return err
- }
-
- _, err = EnsureImage(ctx, client, rawRef, ocispecPlatforms, "always", unpack, options.Quiet, options)
+ _, err := EnsureImage(ctx, client, rawRef, options)
if err != nil {
return err
}
@@ -54,10 +42,15 @@ func Pull(ctx context.Context, client *containerd.Client, rawRef string, options
}
// EnsureImage pulls an image either from ipfs or from registry.
-func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, ocispecPlatforms []v1.Platform, pull string, unpack *bool, quiet bool, options types.ImagePullOptions) (*imgutil.EnsuredImage, error) {
+func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePullOptions) (*imgutil.EnsuredImage, error) {
var ensured *imgutil.EnsuredImage
- if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil {
+ parsedReference, err := referenceutil.Parse(rawRef)
+ if err != nil {
+ return nil, err
+ }
+
+ if parsedReference.Protocol != "" {
if options.VerifyOptions.Provider != "none" {
return nil, errors.New("--verify flag is not supported on IPFS as of now")
}
@@ -75,8 +68,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string,
ipfsPath = dir
}
- ensured, err = ipfs.EnsureImage(ctx, client, options.Stdout, options.Stderr, options.GOptions.Snapshotter, scheme, ref,
- pull, ocispecPlatforms, unpack, quiet, ipfsPath)
+ ensured, err = ipfs.EnsureImage(ctx, client, string(parsedReference.Protocol), parsedReference.String(), ipfsPath, options)
if err != nil {
return nil, err
}
@@ -88,8 +80,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string,
return nil, err
}
- ensured, err = imgutil.EnsureImage(ctx, client, options.Stdout, options.Stderr, options.GOptions.Snapshotter, ref,
- pull, options.GOptions.InsecureRegistry, options.GOptions.HostsDir, ocispecPlatforms, unpack, quiet)
+ ensured, err = imgutil.EnsureImage(ctx, client, ref, options)
if err != nil {
return nil, err
}
diff --git a/pkg/cmd/image/push.go b/pkg/cmd/image/push.go
index c9029bf17d8..a940f832f99 100644
--- a/pkg/cmd/image/push.go
+++ b/pkg/cmd/image/push.go
@@ -18,41 +18,59 @@ package image
import (
"context"
+ "errors"
"fmt"
"io"
+ "net/http"
"os"
"path/filepath"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/images/converter"
- "github.com/containerd/containerd/reference"
- refdocker "github.com/containerd/containerd/reference/docker"
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
- "github.com/containerd/nerdctl/pkg/imgutil/push"
- "github.com/containerd/nerdctl/pkg/ipfs"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/containerd/nerdctl/pkg/signutil"
+ "github.com/opencontainers/go-digest"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/converter"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/containerd/v2/core/remotes/docker"
+ dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config"
+ "github.com/containerd/containerd/v2/pkg/reference"
+ "github.com/containerd/log"
"github.com/containerd/stargz-snapshotter/estargz"
"github.com/containerd/stargz-snapshotter/estargz/zstdchunked"
estargzconvert "github.com/containerd/stargz-snapshotter/nativeconverter/estargz"
- "github.com/opencontainers/go-digest"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/push"
+ "github.com/containerd/nerdctl/v2/pkg/ipfs"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
+ "github.com/containerd/nerdctl/v2/pkg/signutil"
+ "github.com/containerd/nerdctl/v2/pkg/snapshotterutil"
)
// Push pushes an image specified by `rawRef`.
func Push(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePushOptions) error {
- if scheme, ref, err := referenceutil.ParseIPFSRefWithScheme(rawRef); err == nil {
- if scheme != "ipfs" {
- return fmt.Errorf("ipfs scheme is only supported but got %q", scheme)
+ parsedReference, err := referenceutil.Parse(rawRef)
+ if err != nil {
+ return err
+ }
+
+ if parsedReference.Protocol != "" {
+ if parsedReference.Protocol != referenceutil.IPFSProtocol {
+ return fmt.Errorf("ipfs scheme is only supported but got %q", parsedReference.Protocol)
+ }
+ log.G(ctx).Infof("pushing image %q to IPFS", parsedReference)
+
+ // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3489
+ // XXX what if the image is a CID, or only otherwise available on ipfs?
+ err = EnsureAllContent(ctx, client, parsedReference.String(), options.GOptions)
+ if err != nil {
+ return err
}
- logrus.Infof("pushing image %q to IPFS", ref)
var ipfsPath string
if options.IpfsAddress != "" {
@@ -71,21 +89,21 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
if options.Estargz {
layerConvert = eStargzConvertFunc()
}
- c, err := ipfs.Push(ctx, client, ref, layerConvert, options.AllPlatforms, options.Platforms, options.IpfsEnsureImage, ipfsPath)
+ c, err := ipfs.Push(ctx, client, parsedReference.String(), layerConvert, options.AllPlatforms, options.Platforms, options.IpfsEnsureImage, ipfsPath)
if err != nil {
- logrus.WithError(err).Warnf("ipfs push failed")
+ log.G(ctx).WithError(err).Warnf("ipfs push failed")
return err
}
fmt.Fprintln(options.Stdout, c)
return nil
}
- named, err := refdocker.ParseDockerRef(rawRef)
+ parsedReference, err = referenceutil.Parse(rawRef)
if err != nil {
return err
}
- ref := named.String()
- refDomain := refdocker.Domain(named)
+ ref := parsedReference.String()
+ refDomain := parsedReference.Domain
platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platforms)
if err != nil {
@@ -104,7 +122,7 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
return fmt.Errorf("failed to create a tmp reduced-platform image %q (platform=%v): %w", pushRef, options.Platforms, err)
}
defer client.ImageService().Delete(ctx, platImg.Name, images.SynchronousDelete())
- logrus.Infof("pushing as a reduced-platform image (%s, %s)", platImg.Target.MediaType, platImg.Target.Digest)
+ log.G(ctx).Infof("pushing as a reduced-platform image (%s, %s)", platImg.Target.MediaType, platImg.Target.Digest)
}
if options.Estargz {
@@ -114,30 +132,45 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
return fmt.Errorf("failed to convert to eStargz: %v", err)
}
defer client.ImageService().Delete(ctx, esgzImg.Name, images.SynchronousDelete())
- logrus.Infof("pushing as an eStargz image (%s, %s)", esgzImg.Target.MediaType, esgzImg.Target.Digest)
+ log.G(ctx).Infof("pushing as an eStargz image (%s, %s)", esgzImg.Target.MediaType, esgzImg.Target.Digest)
}
+ // In order to push images where most layers are the same but the
+ // repository name is different, it is necessary to refresh the
+ // PushTracker. Otherwise, the MANIFEST_BLOB_UNKNOWN error will occur due
+ // to the registry not creating the corresponding layer link file,
+ // resulting in the failure of the entire image push.
+ pushTracker := docker.NewInMemoryTracker()
+
pushFunc := func(r remotes.Resolver) error {
- return push.Push(ctx, client, r, options.Stdout, pushRef, ref, platMC, options.AllowNondistributableArtifacts, options.Quiet)
+ return push.Push(ctx, client, r, pushTracker, options.Stdout, pushRef, ref, platMC, options.AllowNondistributableArtifacts, options.Quiet)
}
var dOpts []dockerconfigresolver.Opt
if options.GOptions.InsecureRegistry {
- logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain)
+ log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir))
- resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
+
+ ho, err := dockerconfigresolver.NewHostOptions(ctx, refDomain, dOpts...)
if err != nil {
return err
}
+
+ resolverOpts := docker.ResolverOptions{
+ Tracker: pushTracker,
+ Hosts: dockerconfig.ConfigureHosts(ctx, *ho),
+ }
+
+ resolver := docker.NewResolver(resolverOpts)
if err = pushFunc(resolver); err != nil {
// In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused"
- if !errutil.IsErrHTTPResponseToHTTPSClient(err) && !errutil.IsErrConnectionRefused(err) {
+ if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) {
return err
}
if options.GOptions.InsecureRegistry {
- logrus.WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
+ log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...)
if err != nil {
@@ -145,8 +178,8 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
}
return pushFunc(resolver)
}
- logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
- logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
+ log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
+ log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
return err
}
@@ -164,6 +197,14 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, options
options.SignOptions); err != nil {
return err
}
+ if options.GOptions.Snapshotter == "soci" {
+ if err = snapshotterutil.CreateSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms, options.SociOptions); err != nil {
+ return err
+ }
+ if err = snapshotterutil.PushSoci(ref, options.GOptions, options.AllPlatforms, options.Platforms); err != nil {
+ return err
+ }
+ }
if options.Quiet {
fmt.Fprintln(options.Stdout, ref)
}
@@ -174,14 +215,14 @@ func eStargzConvertFunc() converter.ConvertFunc {
convertToESGZ := estargzconvert.LayerConvertFunc()
return func(ctx context.Context, cs content.Store, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
if isReusableESGZ(ctx, cs, desc) {
- logrus.Infof("reusing estargz %s without conversion", desc.Digest)
+ log.L.Infof("reusing estargz %s without conversion", desc.Digest)
return nil, nil
}
newDesc, err := convertToESGZ(ctx, cs, desc)
if err != nil {
return nil, err
}
- logrus.Infof("converted %q to %s", desc.MediaType, newDesc.Digest)
+ log.L.Infof("converted %q to %s", desc.MediaType, newDesc.Digest)
return newDesc, err
}
diff --git a/pkg/cmd/image/remove.go b/pkg/cmd/image/remove.go
index f2aab2058e4..6b9f78fd757 100644
--- a/pkg/cmd/image/remove.go
+++ b/pkg/cmd/image/remove.go
@@ -22,13 +22,14 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
)
// Remove removes a list of `images`.
@@ -64,12 +65,32 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio
walker := &imagewalker.ImageWalker{
Client: client,
OnFound: func(ctx context.Context, found imagewalker.Found) error {
- // if found multiple images, return error unless in force-mode and
- // there is only 1 unique image.
- if found.MatchCount > 1 && !(options.Force && found.UniqueImages == 1) {
- return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ if found.NameMatchIndex == -1 {
+ // if found multiple images, return error unless in force-mode and
+ // there is only 1 unique image.
+ if found.MatchCount > 1 && !(options.Force && found.UniqueImages == 1) {
+ return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ } else if found.NameMatchIndex != found.MatchIndex {
+ // when there is an image with a name matching the argument but the argument is a digest short id,
+ // the deletion process is not performed.
+ return nil
}
+
if cid, ok := runningImages[found.Image.Name]; ok {
+ if options.Force {
+ if err = is.Delete(ctx, found.Image.Name); err != nil {
+ return err
+ }
+ fmt.Fprintf(options.Stdout, "Untagged: %s\n", found.Image.Name)
+ fmt.Fprintf(options.Stdout, "Untagged: %s\n", found.Image.Target.Digest.String())
+
+ found.Image.Name = ":"
+ if _, err = is.Create(ctx, found.Image); err != nil {
+ return err
+ }
+ return nil
+ }
return fmt.Errorf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", found.Req, cid)
}
if cid, ok := usedImages[found.Image.Name]; ok && !options.Force {
@@ -78,7 +99,7 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio
// digests is used only for emulating human-readable output of `docker rmi`
digests, err := found.Image.RootFS(ctx, cs, platforms.DefaultStrict())
if err != nil {
- logrus.WithError(err).Warning("failed to enumerate rootfs")
+ log.G(ctx).WithError(err).Warning("failed to enumerate rootfs")
}
if err := is.Delete(ctx, found.Image.Name, delOpts...); err != nil {
@@ -90,12 +111,64 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio
}
return nil
},
+ OnFoundCriRm: func(ctx context.Context, found imagewalker.Found) (bool, error) {
+ if found.NameMatchIndex == -1 {
+ // if found multiple images, return error unless in force-mode and
+ // there is only 1 unique image.
+ if found.MatchCount > 1 && !(options.Force && found.UniqueImages == 1) {
+ return false, fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ } else if found.NameMatchIndex != found.MatchIndex {
+ // when there is an image with a name matching the argument but the argument is a digest short id,
+ // the deletion process is not performed.
+ return false, nil
+ }
+
+ if cid, ok := runningImages[found.Image.Name]; ok {
+ if options.Force {
+ if err = is.Delete(ctx, found.Image.Name); err != nil {
+ return false, err
+ }
+ fmt.Fprintf(options.Stdout, "Untagged: %s\n", found.Image.Name)
+ fmt.Fprintf(options.Stdout, "Untagged: %s\n", found.Image.Target.Digest.String())
+
+ found.Image.Name = ":"
+ if _, err = is.Create(ctx, found.Image); err != nil {
+ return false, err
+ }
+ return false, nil
+ }
+ return false, fmt.Errorf("conflict: unable to delete %s (cannot be forced) - image is being used by running container %s", found.Req, cid)
+ }
+ if cid, ok := usedImages[found.Image.Name]; ok && !options.Force {
+ return false, fmt.Errorf("conflict: unable to delete %s (must be forced) - image is being used by stopped container %s", found.Req, cid)
+ }
+ // digests is used only for emulating human-readable output of `docker rmi`
+ digests, err := found.Image.RootFS(ctx, cs, platforms.DefaultStrict())
+ if err != nil {
+ log.G(ctx).WithError(err).Warning("failed to enumerate rootfs")
+ }
+
+ if err := is.Delete(ctx, found.Image.Name, delOpts...); err != nil {
+ return false, err
+ }
+ fmt.Fprintf(options.Stdout, "Untagged: %s@%s\n", found.Image.Name, found.Image.Target.Digest)
+ for _, digest := range digests {
+ fmt.Fprintf(options.Stdout, "Deleted: %s\n", digest)
+ }
+ return true, nil
+ },
}
var errs []string
var fatalErr bool
for _, req := range args {
- n, err := walker.Walk(ctx, req)
+ var n int
+ if options.GOptions.KubeHideDupe && options.GOptions.Namespace == "k8s.io" {
+ n, err = walker.WalkCriRm(ctx, req)
+ } else {
+ n, err = walker.Walk(ctx, req)
+ }
if err != nil {
fatalErr = true
}
@@ -112,7 +185,7 @@ func Remove(ctx context.Context, client *containerd.Client, args []string, optio
if !options.Force || fatalErr {
return errors.New(msg)
}
- logrus.Error(msg)
+ log.G(ctx).Error(msg)
}
return nil
}
diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go
index cf4dbf3069e..2b5d6d125ae 100644
--- a/pkg/cmd/image/save.go
+++ b/pkg/cmd/image/save.go
@@ -20,12 +20,13 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images/archive"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/strutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images/archive"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// Save exports `images` to a `io.Writer` (e.g., a file writer, or os.Stdout) specified by `options.Stdout`.
@@ -47,10 +48,16 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio
if found.UniqueImages > 1 {
return fmt.Errorf("ambiguous digest ID: multiple IDs found with provided prefix %s", found.Req)
}
+
+ // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425
+ err = EnsureAllContent(ctx, client, found.Image.Name, options.GOptions)
+ if err != nil {
+ return err
+ }
+
imgName := found.Image.Name
- imgDigest := found.Image.Target.Digest.String()
- if _, ok := savedImages[imgDigest]; !ok {
- savedImages[imgDigest] = struct{}{}
+ if _, ok := savedImages[imgName]; !ok {
+ savedImages[imgName] = struct{}{}
exportOpts = append(exportOpts, archive.WithImage(imageStore, imgName))
}
return nil
diff --git a/pkg/cmd/image/tag.go b/pkg/cmd/image/tag.go
index 8349fc6c769..5323080f745 100644
--- a/pkg/cmd/image/tag.go
+++ b/pkg/cmd/image/tag.go
@@ -20,17 +20,19 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/referenceutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagOptions) error {
imageService := client.ImageService()
var srcName string
- imagewalker := &imagewalker.ImageWalker{
+ walker := &imagewalker.ImageWalker{
Client: client,
OnFound: func(ctx context.Context, found imagewalker.Found) error {
if srcName == "" {
@@ -39,7 +41,7 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO
return nil
},
}
- matchCount, err := imagewalker.Walk(ctx, options.Source)
+ matchCount, err := walker.Walk(ctx, options.Source)
if err != nil {
return err
}
@@ -47,7 +49,7 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO
return fmt.Errorf("%s: not found", options.Source)
}
- target, err := referenceutil.ParseDockerRef(options.Target)
+ parsedReference, err := referenceutil.Parse(options.Target)
if err != nil {
return err
}
@@ -58,17 +60,25 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO
}
defer done(ctx)
- image, err := imageService.Get(ctx, srcName)
+ // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425
+ err = EnsureAllContent(ctx, client, srcName, options.GOptions)
+ if err != nil {
+ log.G(ctx).Warn("Unable to fetch missing layers before committing. " +
+ "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.")
+ }
+
+ img, err := imageService.Get(ctx, srcName)
if err != nil {
return err
}
- image.Name = target.String()
- if _, err = imageService.Create(ctx, image); err != nil {
+
+ img.Name = parsedReference.String()
+ if _, err = imageService.Create(ctx, img); err != nil {
if errdefs.IsAlreadyExists(err) {
- if err = imageService.Delete(ctx, image.Name); err != nil {
+ if err = imageService.Delete(ctx, img.Name); err != nil {
return err
}
- if _, err = imageService.Create(ctx, image); err != nil {
+ if _, err = imageService.Create(ctx, img); err != nil {
return err
}
} else {
diff --git a/pkg/cmd/ipfs/registry_serve.go b/pkg/cmd/ipfs/registry_serve.go
index 09d214a4633..09294032c1d 100644
--- a/pkg/cmd/ipfs/registry_serve.go
+++ b/pkg/cmd/ipfs/registry_serve.go
@@ -21,9 +21,10 @@ import (
"os"
"path/filepath"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/ipfs"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/ipfs"
)
func RegistryServe(options types.IPFSRegistryServeOptions) error {
@@ -47,7 +48,7 @@ func RegistryServe(options types.IPFSRegistryServeOptions) error {
if err != nil {
return err
}
- logrus.Infof("serving on %v", options.ListenRegistry)
+ log.L.Infof("serving on %v", options.ListenRegistry)
http.Handle("/", h)
return http.ListenAndServe(options.ListenRegistry, nil)
}
diff --git a/pkg/cmd/login/login.go b/pkg/cmd/login/login.go
index f4f49b0df2e..773bf8edc76 100644
--- a/pkg/cmd/login/login.go
+++ b/pkg/cmd/login/login.go
@@ -17,28 +17,23 @@
package login
import (
- "bufio"
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
- "os"
- "strings"
- "github.com/containerd/containerd/remotes/docker"
- "github.com/containerd/containerd/remotes/docker/config"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
- dockercliconfig "github.com/docker/cli/cli/config"
- dockercliconfigtypes "github.com/docker/cli/cli/config/types"
- "github.com/docker/docker/api/types/registry"
- "github.com/docker/docker/errdefs"
- "github.com/sirupsen/logrus"
"golang.org/x/net/context/ctxhttp"
- "golang.org/x/term"
+
+ "github.com/containerd/containerd/v2/core/remotes/docker"
+ "github.com/containerd/containerd/v2/core/remotes/docker/config"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
)
const unencryptedPasswordWarning = `WARNING: Your password will be stored unencrypted in %s.
@@ -46,121 +41,90 @@ Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
`
-type isFileStore interface {
- IsFileStore() bool
- GetFilename() string
-}
-
func Login(ctx context.Context, options types.LoginCommandOptions, stdout io.Writer) error {
- var serverAddress string
- if options.ServerAddress == "" {
- serverAddress = dockerconfigresolver.IndexServer
- } else {
- serverAddress = options.ServerAddress
+ registryURL, err := dockerconfigresolver.Parse(options.ServerAddress)
+ if err != nil {
+ return err
+ }
+
+ credStore, err := dockerconfigresolver.NewCredentialsStore("")
+ if err != nil {
+ return err
}
var responseIdentityToken string
- isDefaultRegistry := serverAddress == dockerconfigresolver.IndexServer
- authConfig, err := GetDefaultAuthConfig(options.Username == "" && options.Password == "", serverAddress, isDefaultRegistry)
- if authConfig == nil {
- authConfig = ®istry.AuthConfig{ServerAddress: serverAddress}
- }
- if err == nil && authConfig.Username != "" && authConfig.Password != "" {
- //login With StoreCreds
- responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
+ credentials, err := credStore.Retrieve(registryURL, options.Username == "" && options.Password == "")
+ credentials.IdentityToken = ""
+
+ if err == nil && credentials.Username != "" && credentials.Password != "" {
+ responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
}
- if err != nil || authConfig.Username == "" || authConfig.Password == "" {
- err = ConfigureAuthentication(authConfig, options.Username, options.Password)
+ if err != nil || credentials.Username == "" || credentials.Password == "" {
+ err = promptUserForAuthentication(credentials, options.Username, options.Password, stdout)
if err != nil {
return err
}
- responseIdentityToken, err = loginClientSide(ctx, options.GOptions, *authConfig)
+ responseIdentityToken, err = loginClientSide(ctx, options.GOptions, registryURL, credentials)
if err != nil {
return err
}
}
if responseIdentityToken != "" {
- authConfig.Password = ""
- authConfig.IdentityToken = responseIdentityToken
- }
-
- dockerConfigFile, err := dockercliconfig.Load("")
- if err != nil {
- return err
+ credentials.Password = ""
+ credentials.IdentityToken = responseIdentityToken
}
- creds := dockerConfigFile.GetCredentialsStore(serverAddress)
-
- store, isFile := creds.(isFileStore)
// Display a warning if we're storing the users password (not a token) and credentials store type is file.
- if isFile && authConfig.Password != "" {
- _, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, store.GetFilename()))
+ storageFileLocation := credStore.FileStorageLocation(registryURL)
+ if storageFileLocation != "" && credentials.Password != "" {
+ _, err = fmt.Fprintln(stdout, fmt.Sprintf(unencryptedPasswordWarning, storageFileLocation))
if err != nil {
return err
}
}
- if err := creds.Store(dockercliconfigtypes.AuthConfig(*(authConfig))); err != nil {
+ err = credStore.Store(registryURL, credentials)
+ if err != nil {
return fmt.Errorf("error saving credentials: %w", err)
}
- fmt.Fprintln(stdout, "Login Succeeded")
-
- return nil
-}
-
-// Code from github.com/docker/cli/cli/command (v20.10.3)
-// GetDefaultAuthConfig gets the default auth config given a serverAddress
-// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
-func GetDefaultAuthConfig(checkCredStore bool, serverAddress string, isDefaultRegistry bool) (*registry.AuthConfig, error) {
- if !isDefaultRegistry {
- var err error
- serverAddress, err = convertToHostname(serverAddress)
+ // When the port is the https default (443), other clients cannot be expected to necessarily lookup the variants with port
+ // so save it both with and without port.
+ // This is the case for at least buildctl: https://github.com/containerd/nerdctl/issues/3748
+ if registryURL.Port() == dockerconfigresolver.StandardHTTPSPort {
+ registryURL.Host = registryURL.Hostname()
+ err = credStore.Store(registryURL, credentials)
if err != nil {
- return nil, err
+ return fmt.Errorf("error saving credentials: %w", err)
}
}
- var authconfig = dockercliconfigtypes.AuthConfig{}
- if checkCredStore {
- dockerConfigFile, err := dockercliconfig.Load("")
- if err != nil {
- return nil, err
- }
- authconfig, err = dockerConfigFile.GetAuthConfig(serverAddress)
- if err != nil {
- return nil, err
- }
- }
- authconfig.ServerAddress = serverAddress
- authconfig.IdentityToken = ""
- res := registry.AuthConfig(authconfig)
- return &res, nil
+
+ _, err = fmt.Fprintln(stdout, "Login Succeeded")
+
+ return err
}
-func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, auth registry.AuthConfig) (string, error) {
- host, err := convertToHostname(auth.ServerAddress)
- if err != nil {
- return "", err
- }
+func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptions, registryURL *dockerconfigresolver.RegistryURL, credentials *dockerconfigresolver.Credentials) (string, error) {
+ host := registryURL.Host
var dOpts []dockerconfigresolver.Opt
if globalOptions.InsecureRegistry {
- logrus.Warnf("skipping verifying HTTPS certs for %q", host)
+ log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", host)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(globalOptions.HostsDir))
authCreds := func(acArg string) (string, string, error) {
if acArg == host {
- if auth.RegistryToken != "" {
+ if credentials.RegistryToken != "" {
// Even containerd/CRI does not support RegistryToken as of v1.4.3,
// so, nobody is actually using RegistryToken?
- logrus.Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host)
+ log.G(ctx).Warnf("RegistryToken (for %q) is not supported yet (FIXME)", host)
}
- return auth.Username, auth.Password, nil
+ return credentials.Username, credentials.Password, nil
}
return "", "", fmt.Errorf("expected acArg to be %q, got %q", host, acArg)
}
@@ -180,13 +144,13 @@ func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptio
if err != nil {
return "", err
}
- logrus.Debugf("len(regHosts)=%d", len(regHosts))
+ log.G(ctx).Debugf("len(regHosts)=%d", len(regHosts))
if len(regHosts) == 0 {
return "", fmt.Errorf("got empty []docker.RegistryHost for %q", host)
}
for i, rh := range regHosts {
err = tryLoginWithRegHost(ctx, rh)
- if err != nil && globalOptions.InsecureRegistry && (errutil.IsErrHTTPResponseToHTTPSClient(err) || errutil.IsErrConnectionRefused(err)) {
+ if err != nil && globalOptions.InsecureRegistry && (errors.Is(err, http.ErrSchemeMismatch) || errutil.IsErrConnectionRefused(err)) {
rh.Scheme = "http"
err = tryLoginWithRegHost(ctx, rh)
}
@@ -194,7 +158,7 @@ func loginClientSide(ctx context.Context, globalOptions types.GlobalCommandOptio
if err == nil {
return identityToken, nil
}
- logrus.WithError(err).WithField("i", i).Error("failed to call tryLoginWithRegHost")
+ log.G(ctx).WithError(err).WithField("i", i).Error("failed to call tryLoginWithRegHost")
}
return "", err
}
@@ -248,76 +212,3 @@ func tryLoginWithRegHost(ctx context.Context, rh docker.RegistryHost) error {
return errors.New("too many 401 (probably)")
}
-
-func ConfigureAuthentication(authConfig *registry.AuthConfig, username, password string) error {
- authConfig.Username = strings.TrimSpace(authConfig.Username)
- if username = strings.TrimSpace(username); username == "" {
- username = authConfig.Username
- }
- if username == "" {
- fmt.Print("Enter Username: ")
- usr, err := readUsername()
- if err != nil {
- return err
- }
- username = usr
- }
- if username == "" {
- return fmt.Errorf("error: Username is Required")
- }
-
- if password == "" {
- fmt.Print("Enter Password: ")
- pwd, err := readPassword()
- fmt.Println()
- if err != nil {
- return err
- }
- password = pwd
- }
- if password == "" {
- return fmt.Errorf("error: Password is Required")
- }
-
- authConfig.Username = username
- authConfig.Password = password
-
- return nil
-}
-
-func readUsername() (string, error) {
- var fd *os.File
- if term.IsTerminal(int(os.Stdin.Fd())) {
- fd = os.Stdin
- } else {
- return "", fmt.Errorf("stdin is not a terminal (Hint: use `nerdctl login --username=USERNAME --password-stdin`)")
- }
-
- reader := bufio.NewReader(fd)
- username, err := reader.ReadString('\n')
- if err != nil {
- return "", fmt.Errorf("error reading username: %w", err)
- }
- username = strings.TrimSpace(username)
-
- return username, nil
-}
-
-func convertToHostname(serverAddress string) (string, error) {
- // Ensure that URL contains scheme for a good parsing process
- if strings.Contains(serverAddress, "://") {
- u, err := url.Parse(serverAddress)
- if err != nil {
- return "", err
- }
- serverAddress = u.Host
- } else {
- u, err := url.Parse("https://" + serverAddress)
- if err != nil {
- return "", err
- }
- serverAddress = u.Host
- }
-
- return serverAddress, nil
-}
diff --git a/pkg/cmd/login/prompt.go b/pkg/cmd/login/prompt.go
new file mode 100644
index 00000000000..db1f6554244
--- /dev/null
+++ b/pkg/cmd/login/prompt.go
@@ -0,0 +1,109 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package login
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ "golang.org/x/term"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+)
+
+var (
+ // User did not provide non-empty credentials when prompted for it
+ ErrUsernameIsRequired = errors.New("username is required")
+ ErrPasswordIsRequired = errors.New("password is required")
+
+ // System errors - not a terminal, failure to read, etc
+ ErrReadingUsername = errors.New("unable to read username")
+ ErrReadingPassword = errors.New("unable to read password")
+ ErrNotATerminal = errors.New("stdin is not a terminal (Hint: use `nerdctl login --username=USERNAME --password-stdin`)")
+ ErrCannotAllocateTerminal = errors.New("error allocating terminal")
+)
+
+// promptUserForAuthentication will prompt the user for credentials if needed
+// It might error with any of the errors defined above.
+func promptUserForAuthentication(credentials *dockerconfigresolver.Credentials, username, password string, stdout io.Writer) error {
+ var err error
+
+ // If the provided username is empty...
+ if username = strings.TrimSpace(username); username == "" {
+ // Use the one we know of (from the store)
+ username = credentials.Username
+ // If the one from the store was empty as well, prompt and read the username
+ if username == "" {
+ _, _ = fmt.Fprint(stdout, "Enter Username: ")
+ username, err = readUsername()
+ if err != nil {
+ return err
+ }
+
+ username = strings.TrimSpace(username)
+ // If it still is empty, that is an error
+ if username == "" {
+ return ErrUsernameIsRequired
+ }
+ }
+ }
+
+ // If password was NOT passed along, ask for it
+ if password == "" {
+ _, _ = fmt.Fprint(stdout, "Enter Password: ")
+ password, err = readPassword()
+ if err != nil {
+ return err
+ }
+
+ _, _ = fmt.Fprintln(stdout)
+ password = strings.TrimSpace(password)
+
+ // If nothing was provided, error out
+ if password == "" {
+ return ErrPasswordIsRequired
+ }
+ }
+
+ // Attach non-empty credentials to the auth object and return
+ credentials.Username = username
+ credentials.Password = password
+
+ return nil
+}
+
+// readUsername will try to read from user input
+// It might error with:
+// - ErrNotATerminal
+// - ErrReadingUsername
+func readUsername() (string, error) {
+ fd := os.Stdin
+ if !term.IsTerminal(int(fd.Fd())) {
+ return "", ErrNotATerminal
+ }
+
+ username, err := bufio.NewReader(fd).ReadString('\n')
+ if err != nil {
+ return "", errors.Join(ErrReadingUsername, err)
+ }
+
+ return strings.TrimSpace(username), nil
+}
diff --git a/pkg/cmd/login/login_unix.go b/pkg/cmd/login/prompt_unix.go
similarity index 72%
rename from pkg/cmd/login/login_unix.go
rename to pkg/cmd/login/prompt_unix.go
index ee536be1721..69529473f6d 100644
--- a/pkg/cmd/login/login_unix.go
+++ b/pkg/cmd/login/prompt_unix.go
@@ -1,4 +1,4 @@
-//go:build freebsd || linux
+//go:build unix
/*
Copyright The containerd Authors.
@@ -19,28 +19,34 @@
package login
import (
- "fmt"
+ "errors"
"os"
"syscall"
"golang.org/x/term"
+
+ "github.com/containerd/log"
)
func readPassword() (string, error) {
- var fd int
- if term.IsTerminal(syscall.Stdin) {
- fd = syscall.Stdin
- } else {
+ fd := syscall.Stdin
+ if !term.IsTerminal(fd) {
tty, err := os.Open("/dev/tty")
if err != nil {
- return "", fmt.Errorf("error allocating terminal: %w", err)
+ return "", errors.Join(ErrCannotAllocateTerminal, err)
}
- defer tty.Close()
+ defer func() {
+ err = tty.Close()
+ if err != nil {
+ log.L.WithError(err).Error("failed closing tty")
+ }
+ }()
fd = int(tty.Fd())
}
+
bytePassword, err := term.ReadPassword(fd)
if err != nil {
- return "", fmt.Errorf("error reading password: %w", err)
+ return "", errors.Join(ErrReadingPassword, err)
}
return string(bytePassword), nil
diff --git a/pkg/cmd/login/login_windows.go b/pkg/cmd/login/prompt_windows.go
similarity index 79%
rename from pkg/cmd/login/login_windows.go
rename to pkg/cmd/login/prompt_windows.go
index 89c3834fb92..913e6ff5f98 100644
--- a/pkg/cmd/login/login_windows.go
+++ b/pkg/cmd/login/prompt_windows.go
@@ -17,22 +17,21 @@
package login
import (
- "fmt"
+ "errors"
"syscall"
"golang.org/x/term"
)
func readPassword() (string, error) {
- var fd int
- if term.IsTerminal(int(syscall.Stdin)) {
- fd = int(syscall.Stdin)
- } else {
- return "", fmt.Errorf("error allocating terminal")
+ fd := int(syscall.Stdin)
+ if !term.IsTerminal(fd) {
+ return "", ErrNotATerminal
}
+
bytePassword, err := term.ReadPassword(fd)
if err != nil {
- return "", fmt.Errorf("error reading password: %w", err)
+ return "", errors.Join(ErrReadingPassword, err)
}
return string(bytePassword), nil
diff --git a/pkg/cmd/logout/logout.go b/pkg/cmd/logout/logout.go
new file mode 100644
index 00000000000..99a4b77f852
--- /dev/null
+++ b/pkg/cmd/logout/logout.go
@@ -0,0 +1,46 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package logout
+
+import (
+ "context"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+)
+
+func Logout(ctx context.Context, logoutServer string) (map[string]error, error) {
+ reg, err := dockerconfigresolver.Parse(logoutServer)
+ if err != nil {
+ return nil, err
+ }
+
+ credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
+ if err != nil {
+ return nil, err
+ }
+
+ return credentialsStore.Erase(reg)
+}
+
+func ShellCompletion() ([]string, error) {
+ credentialsStore, err := dockerconfigresolver.NewCredentialsStore("")
+ if err != nil {
+ return nil, err
+ }
+
+ return credentialsStore.ShellCompletion(), nil
+}
diff --git a/pkg/cmd/namespace/create.go b/pkg/cmd/namespace/create.go
index f23765dcbab..f07b9f007e6 100644
--- a/pkg/cmd/namespace/create.go
+++ b/pkg/cmd/namespace/create.go
@@ -19,8 +19,9 @@ package namespace
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
func Create(ctx context.Context, client *containerd.Client, namespace string, options types.NamespaceCreateOptions) error {
diff --git a/pkg/cmd/namespace/inspect.go b/pkg/cmd/namespace/inspect.go
index 99341ca03d2..3a7a4932815 100644
--- a/pkg/cmd/namespace/inspect.go
+++ b/pkg/cmd/namespace/inspect.go
@@ -19,11 +19,12 @@ package namespace
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/namespaces"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
func Inspect(ctx context.Context, client *containerd.Client, inspectedNamespaces []string, options types.NamespaceInspectOptions) error {
diff --git a/pkg/cmd/namespace/namespace_freebsd.go b/pkg/cmd/namespace/namespace_freebsd.go
index a5448749201..a3a45d59168 100644
--- a/pkg/cmd/namespace/namespace_freebsd.go
+++ b/pkg/cmd/namespace/namespace_freebsd.go
@@ -17,7 +17,7 @@
package namespace
import (
- "github.com/containerd/containerd/namespaces"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
)
func namespaceDeleteOpts(cgroup bool) ([]namespaces.DeleteOpts, error) {
diff --git a/pkg/cmd/namespace/namespace_linux.go b/pkg/cmd/namespace/namespace_linux.go
index e13b331fa95..96255cdcb5e 100644
--- a/pkg/cmd/namespace/namespace_linux.go
+++ b/pkg/cmd/namespace/namespace_linux.go
@@ -17,8 +17,8 @@
package namespace
import (
- "github.com/containerd/containerd/namespaces"
- "github.com/containerd/containerd/runtime/opts"
+ "github.com/containerd/containerd/v2/core/runtime/opts"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
)
func namespaceDeleteOpts(cgroup bool) ([]namespaces.DeleteOpts, error) {
diff --git a/pkg/cmd/namespace/namespace_windows.go b/pkg/cmd/namespace/namespace_windows.go
index a5448749201..a3a45d59168 100644
--- a/pkg/cmd/namespace/namespace_windows.go
+++ b/pkg/cmd/namespace/namespace_windows.go
@@ -17,7 +17,7 @@
package namespace
import (
- "github.com/containerd/containerd/namespaces"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
)
func namespaceDeleteOpts(cgroup bool) ([]namespaces.DeleteOpts, error) {
diff --git a/pkg/cmd/namespace/remove.go b/pkg/cmd/namespace/remove.go
index 130e302b9b2..81850be854e 100644
--- a/pkg/cmd/namespace/remove.go
+++ b/pkg/cmd/namespace/remove.go
@@ -20,10 +20,11 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/log"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
func Remove(ctx context.Context, client *containerd.Client, deletedNamespaces []string, options types.NamespaceRemoveOptions) error {
@@ -43,7 +44,7 @@ func Remove(ctx context.Context, client *containerd.Client, deletedNamespaces []
continue
}
}
- _, err := fmt.Fprintf(options.Stdout, "%s\n", target)
+ _, err := fmt.Fprintln(options.Stdout, target)
return err
}
return exitErr
diff --git a/pkg/cmd/namespace/update.go b/pkg/cmd/namespace/update.go
index a8b4eb31aef..63d2d8a5971 100644
--- a/pkg/cmd/namespace/update.go
+++ b/pkg/cmd/namespace/update.go
@@ -19,8 +19,9 @@ package namespace
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
func Update(ctx context.Context, client *containerd.Client, namespace string, options types.NamespaceUpdateOptions) error {
diff --git a/pkg/cmd/network/create.go b/pkg/cmd/network/create.go
index f592a5ee62b..dc62875863e 100644
--- a/pkg/cmd/network/create.go
+++ b/pkg/cmd/network/create.go
@@ -20,29 +20,31 @@ import (
"fmt"
"io"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/netutil"
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
func Create(options types.NetworkCreateOptions, stdout io.Writer) error {
- if options.CreateOptions.Subnet == "" {
- if options.CreateOptions.Gateway != "" || options.CreateOptions.IPRange != "" {
+ if len(options.Subnets) == 0 {
+ if options.Gateway != "" || options.IPRange != "" {
return fmt.Errorf("cannot set gateway or ip-range without subnet, specify --subnet manually")
}
+ options.Subnets = []string{""}
}
- e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath)
+ e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace))
if err != nil {
return err
}
- net, err := e.CreateNetwork(options.CreateOptions)
+ net, err := e.CreateNetwork(options)
if err != nil {
if errdefs.IsAlreadyExists(err) {
- return fmt.Errorf("network with name %s already exists", options.CreateOptions.Name)
+ return fmt.Errorf("network with name %s already exists", options.Name)
}
return err
}
- _, err = fmt.Fprintf(stdout, "%s\n", *net.NerdctlID)
+ _, err = fmt.Fprintln(stdout, *net.NerdctlID)
return err
}
diff --git a/pkg/cmd/network/inspect.go b/pkg/cmd/network/inspect.go
index eb226ac1ec4..5fae28028f3 100644
--- a/pkg/cmd/network/inspect.go
+++ b/pkg/cmd/network/inspect.go
@@ -19,61 +19,71 @@ package network
import (
"context"
"encoding/json"
+ "errors"
"fmt"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/idutil/netwalker"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
func Inspect(ctx context.Context, options types.NetworkInspectOptions) error {
- globalOptions := options.GOptions
- e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath)
+ if options.Mode != "native" && options.Mode != "dockercompat" {
+ return fmt.Errorf("unknown mode %q", options.Mode)
+ }
+ cniEnv, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace))
if err != nil {
return err
}
- if options.Mode != "native" && options.Mode != "dockercompat" {
- return fmt.Errorf("unknown mode %q", options.Mode)
- }
var result []interface{}
- walker := netwalker.NetworkWalker{
- Client: e,
- OnFound: func(ctx context.Context, found netwalker.Found) error {
- if found.MatchCount > 1 {
- return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
- }
- r := &native.Network{
- CNI: json.RawMessage(found.Network.Bytes),
- NerdctlID: found.Network.NerdctlID,
- NerdctlLabels: found.Network.NerdctlLabels,
- File: found.Network.File,
- }
- switch options.Mode {
- case "native":
- result = append(result, r)
- case "dockercompat":
- compat, err := dockercompat.NetworkFromNative(r)
- if err != nil {
- return err
- }
- result = append(result, compat)
+ netLists, errs := cniEnv.ListNetworksMatch(options.Networks, true)
+
+ for req, netList := range netLists {
+ if len(netList) > 1 {
+ errs = append(errs, fmt.Errorf("multiple IDs found with provided prefix: %s", req))
+ continue
+ }
+ if len(netList) == 0 {
+ errs = append(errs, fmt.Errorf("no network found matching: %s", req))
+ continue
+ }
+ network := netList[0]
+ r := &native.Network{
+ CNI: json.RawMessage(network.Bytes),
+ NerdctlID: network.NerdctlID,
+ NerdctlLabels: network.NerdctlLabels,
+ File: network.File,
+ }
+ switch options.Mode {
+ case "native":
+ result = append(result, r)
+ case "dockercompat":
+ compat, err := dockercompat.NetworkFromNative(r)
+ if err != nil {
+ return err
}
- return nil
- },
+ result = append(result, compat)
+ }
}
- // `network inspect` doesn't support pseudo network.
- err = walker.WalkAll(ctx, options.Networks, true, false)
if len(result) > 0 {
if formatErr := formatter.FormatSlice(options.Format, options.Stdout, result); formatErr != nil {
- logrus.Error(formatErr)
+ log.G(ctx).Error(formatErr)
}
+ err = nil
+ } else {
+ err = errors.New("unable to find any network matching the provided request")
+ }
+
+ for _, unErr := range errs {
+ log.G(ctx).Error(unErr)
}
+
return err
}
diff --git a/pkg/cmd/network/list.go b/pkg/cmd/network/list.go
index 3e0cfc54030..b2075356d8c 100644
--- a/pkg/cmd/network/list.go
+++ b/pkg/cmd/network/list.go
@@ -21,12 +21,13 @@ import (
"context"
"errors"
"fmt"
+ "strings"
"text/tabwriter"
"text/template"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
type networkPrintable struct {
@@ -42,6 +43,7 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
quiet := options.Quiet
format := options.Format
w := options.Stdout
+ filters := options.Filters
var tmpl *template.Template
switch format {
@@ -63,7 +65,7 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
}
}
- e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath)
+ e, err := netutil.NewCNIEnv(globalOptions.CNIPath, globalOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace))
if err != nil {
return err
}
@@ -71,6 +73,21 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
if err != nil {
return err
}
+
+ labelFilterFuncs, nameFilterFuncs, err := getNetworkFilterFuncs(filters)
+ if err != nil {
+ return err
+ }
+ if len(filters) > 0 {
+ filtered := make([]*netutil.NetworkConfig, 0)
+ for _, net := range netConfigs {
+ if networkMatchesFilter(net, labelFilterFuncs, nameFilterFuncs) {
+ filtered = append(filtered, net)
+ }
+ }
+ netConfigs = filtered
+ }
+
pp := make([]networkPrintable, len(netConfigs))
for i, n := range netConfigs {
p := networkPrintable{
@@ -90,14 +107,16 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
}
// append pseudo networks
- pp = append(pp, []networkPrintable{
- {
- Name: "host",
- },
- {
- Name: "none",
- },
- }...)
+ if len(filters) == 0 { // filter a pseudo networks is meanless
+ pp = append(pp, []networkPrintable{
+ {
+ Name: "host",
+ },
+ {
+ Name: "none",
+ },
+ }...)
+ }
for _, p := range pp {
if tmpl != nil {
@@ -105,7 +124,7 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
if err := tmpl.Execute(&b, p); err != nil {
return err
}
- if _, err = fmt.Fprintf(w, b.String()+"\n"); err != nil {
+ if _, err = fmt.Fprintln(w, b.String()); err != nil {
return err
}
} else if quiet {
@@ -121,3 +140,56 @@ func List(ctx context.Context, options types.NetworkListOptions) error {
}
return nil
}
+
+func getNetworkFilterFuncs(filters []string) ([]func(*map[string]string) bool, []func(string) bool, error) {
+ labelFilterFuncs := make([]func(*map[string]string) bool, 0)
+ nameFilterFuncs := make([]func(string) bool, 0)
+
+ for _, filter := range filters {
+ if strings.HasPrefix(filter, "name") || strings.HasPrefix(filter, "label") {
+ subs := strings.SplitN(filter, "=", 2)
+ if len(subs) < 2 {
+ continue
+ }
+ switch subs[0] {
+ case "name":
+ nameFilterFuncs = append(nameFilterFuncs, func(name string) bool {
+ return strings.Contains(name, subs[1])
+ })
+ case "label":
+ v, k, hasValue := "", subs[1], false
+ if subs := strings.SplitN(subs[1], "=", 2); len(subs) == 2 {
+ hasValue = true
+ k, v = subs[0], subs[1]
+ }
+ labelFilterFuncs = append(labelFilterFuncs, func(labels *map[string]string) bool {
+ if labels == nil {
+ return false
+ }
+ val, ok := (*labels)[k]
+ if !ok || (hasValue && val != v) {
+ return false
+ }
+ return true
+ })
+ }
+ continue
+ }
+ }
+ return labelFilterFuncs, nameFilterFuncs, nil
+}
+
+func networkMatchesFilter(net *netutil.NetworkConfig, labelFilterFuncs []func(*map[string]string) bool, nameFilterFuncs []func(string) bool) bool {
+ for _, labelFilterFunc := range labelFilterFuncs {
+ if !labelFilterFunc(net.NerdctlLabels) {
+ return false
+ }
+ }
+ for _, nameFilterFunc := range nameFilterFuncs {
+ if !nameFilterFunc(net.Name) {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/pkg/cmd/network/prune.go b/pkg/cmd/network/prune.go
index 84b59a8776c..d88442160b1 100644
--- a/pkg/cmd/network/prune.go
+++ b/pkg/cmd/network/prune.go
@@ -20,15 +20,16 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func Prune(ctx context.Context, client *containerd.Client, options types.NetworkPruneOptions) error {
- e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath)
+ e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace))
if err != nil {
return err
}
@@ -55,7 +56,7 @@ func Prune(ctx context.Context, client *containerd.Client, options types.Network
continue
}
if err := e.RemoveNetwork(net); err != nil {
- logrus.WithError(err).Errorf("failed to remove network %s", net.Name)
+ log.G(ctx).WithError(err).Errorf("failed to remove network %s", net.Name)
continue
}
removedNetworks = append(removedNetworks, net.Name)
diff --git a/pkg/cmd/network/remove.go b/pkg/cmd/network/remove.go
index 76da1eeabca..41731d3f40c 100644
--- a/pkg/cmd/network/remove.go
+++ b/pkg/cmd/network/remove.go
@@ -18,16 +18,18 @@ package network
import (
"context"
+ "errors"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/idutil/netwalker"
- "github.com/containerd/nerdctl/pkg/netutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
func Remove(ctx context.Context, client *containerd.Client, options types.NetworkRemoveOptions) error {
- e, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath)
+ cniEnv, err := netutil.NewCNIEnv(options.GOptions.CNIPath, options.GOptions.CNINetConfPath, netutil.WithNamespace(options.GOptions.Namespace))
if err != nil {
return err
}
@@ -37,28 +39,52 @@ func Remove(ctx context.Context, client *containerd.Client, options types.Networ
return err
}
- walker := netwalker.NetworkWalker{
- Client: e,
- OnFound: func(ctx context.Context, found netwalker.Found) error {
- if found.MatchCount > 1 {
- return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
- }
- if value, ok := usedNetworkInfo[found.Network.Name]; ok {
- return fmt.Errorf("network %q is in use by container %q", found.Req, value)
- }
- if found.Network.NerdctlID == nil {
- return fmt.Errorf("%s is managed outside nerdctl and cannot be removed", found.Req)
- }
- if found.Network.File == "" {
- return fmt.Errorf("%s is a pre-defined network and cannot be removed", found.Req)
- }
- if err := e.RemoveNetwork(found.Network); err != nil {
- return err
- }
- fmt.Fprintln(options.Stdout, found.Req)
- return nil
- },
+ var result []string
+ netLists, errs := cniEnv.ListNetworksMatch(options.Networks, false)
+
+ for req, netList := range netLists {
+ if len(netList) > 1 {
+ errs = append(errs, fmt.Errorf("multiple IDs found with provided prefix: %s", req))
+ continue
+ }
+ if len(netList) == 0 {
+ errs = append(errs, fmt.Errorf("no network found matching: %s", req))
+ continue
+ }
+ network := netList[0]
+ if value, ok := usedNetworkInfo[network.Name]; ok {
+ errs = append(errs, fmt.Errorf("network %q is in use by container %q", req, value))
+ continue
+ }
+ if network.Name == "bridge" {
+ errs = append(errs, errors.New("cannot remove pre-defined network bridge"))
+ continue
+ }
+ if network.File == "" {
+ errs = append(errs, fmt.Errorf("%s is a pre-defined network and cannot be removed", req))
+ continue
+ }
+ if network.NerdctlID == nil {
+ errs = append(errs, fmt.Errorf("%s is managed outside nerdctl and cannot be removed", req))
+ continue
+ }
+ if err := cniEnv.RemoveNetwork(network); err != nil {
+ errs = append(errs, err)
+ } else {
+ result = append(result, req)
+ }
+ }
+ for _, unErr := range errs {
+ log.G(ctx).Error(unErr)
+ }
+ if len(result) > 0 {
+ for _, id := range result {
+ fmt.Fprintln(options.Stdout, id)
+ }
+ err = nil
+ } else {
+ err = errors.New("no network could be removed")
}
- return walker.WalkAll(ctx, options.Networks, true, false)
+ return err
}
diff --git a/pkg/cmd/system/events.go b/pkg/cmd/system/events.go
index 22083bfe2a6..a544f071b57 100644
--- a/pkg/cmd/system/events.go
+++ b/pkg/cmd/system/events.go
@@ -22,26 +22,128 @@ import (
"encoding/json"
"errors"
"fmt"
+ "strings"
"text/template"
"time"
- "github.com/containerd/containerd"
_ "github.com/containerd/containerd/api/events" // Register grpc event types
- "github.com/containerd/containerd/events"
- "github.com/containerd/containerd/log"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/events"
+ "github.com/containerd/log"
"github.com/containerd/typeurl/v2"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
)
// EventOut contains information about an event.
type EventOut struct {
Timestamp time.Time
+ ID string
Namespace string
Topic string
+ Status Status
Event string
}
+type Status string
+
+const (
+ START Status = "start"
+ UNKNOWN Status = "unknown"
+)
+
+var statuses = [...]Status{START, UNKNOWN}
+
+func isStatus(status string) bool {
+ status = strings.ToLower(status)
+
+ for _, supportedStatus := range statuses {
+ if string(supportedStatus) == status {
+ return true
+ }
+ }
+
+ return false
+}
+
+func TopicToStatus(topic string) Status {
+ if strings.Contains(strings.ToLower(topic), string(START)) {
+ return START
+ }
+
+ return UNKNOWN
+}
+
+// EventFilter for filtering events
+type EventFilter func(*EventOut) bool
+
+// generateEventFilter is similar to Podman implementation:
+// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
+func generateEventFilter(filter, filterValue string) (func(e *EventOut) bool, error) {
+ switch strings.ToUpper(filter) {
+ case "EVENT", "STATUS":
+ return func(e *EventOut) bool {
+ if !isStatus(string(e.Status)) {
+ return false
+ }
+
+ return strings.EqualFold(string(e.Status), filterValue)
+ }, nil
+ }
+
+ return nil, fmt.Errorf("%s is an invalid or unsupported filter", filter)
+}
+
+// parseFilter is similar to Podman implementation:
+// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L96
+func parseFilter(filter string) (string, string, error) {
+ filterSplit := strings.SplitN(filter, "=", 2)
+ if len(filterSplit) != 2 {
+ return "", "", fmt.Errorf("%s is an invalid filter", filter)
+ }
+ return filterSplit[0], filterSplit[1], nil
+}
+
+// applyFilters is similar to Podman implementation:
+// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L106
+func applyFilters(event *EventOut, filterMap map[string][]EventFilter) bool {
+ for _, filters := range filterMap {
+ match := false
+ for _, filter := range filters {
+ if filter(event) {
+ match = true
+ break
+ }
+ }
+ if !match {
+ return false
+ }
+ }
+ return true
+}
+
+// generateEventFilters is similar to Podman implementation:
+// https://github.com/containers/podman/blob/189d862d54b3824c74bf7474ddfed6de69ec5a09/libpod/events/filters.go#L11
+func generateEventFilters(filters []string) (map[string][]EventFilter, error) {
+ filterMap := make(map[string][]EventFilter)
+ for _, filter := range filters {
+ key, val, err := parseFilter(filter)
+ if err != nil {
+ return nil, err
+ }
+ filterFunc, err := generateEventFilter(key, val)
+ if err != nil {
+ return nil, err
+ }
+ filterSlice := filterMap[key]
+ filterSlice = append(filterSlice, filterFunc)
+ filterMap[key] = filterSlice
+ }
+
+ return filterMap, nil
+}
+
// Events is from https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/events/events.go
func Events(ctx context.Context, client *containerd.Client, options types.SystemEventsOptions) error {
eventsClient := client.EventService()
@@ -59,6 +161,10 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
return err
}
}
+ filterMap, err := generateEventFilters(options.Filters)
+ if err != nil {
+ return err
+ }
for {
var e *events.Envelope
select {
@@ -68,6 +174,7 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
}
if e != nil {
var out []byte
+ var id string
if e.Event != nil {
v, err := typeurl.UnmarshalAny(e.Event)
if err != nil {
@@ -80,26 +187,41 @@ func Events(ctx context.Context, client *containerd.Client, options types.System
continue
}
}
- if tmpl != nil {
- out := EventOut{e.Timestamp, e.Namespace, e.Topic, string(out)}
- var b bytes.Buffer
- if err := tmpl.Execute(&b, out); err != nil {
- return err
- }
- if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
- return err
- }
+ var data map[string]interface{}
+ err := json.Unmarshal(out, &data)
+ if err != nil {
+ log.G(ctx).WithError(err).Warn("cannot marshal Any into JSON")
} else {
- if _, err := fmt.Fprintln(
- options.Stdout,
- e.Timestamp,
- e.Namespace,
- e.Topic,
- string(out),
- ); err != nil {
- return err
+ _, ok := data["container_id"]
+ if ok {
+ id = data["container_id"].(string)
}
}
+
+ eOut := EventOut{e.Timestamp, id, e.Namespace, e.Topic, TopicToStatus(e.Topic), string(out)}
+ match := applyFilters(&eOut, filterMap)
+ if match {
+ if tmpl != nil {
+ var b bytes.Buffer
+ if err := tmpl.Execute(&b, eOut); err != nil {
+ return err
+ }
+ if _, err := fmt.Fprintln(options.Stdout, b.String()+"\n"); err != nil {
+ return err
+ }
+ } else {
+ if _, err := fmt.Fprintln(
+ options.Stdout,
+ e.Timestamp,
+ e.Namespace,
+ e.Topic,
+ string(out),
+ ); err != nil {
+ return err
+ }
+ }
+ }
+
}
}
}
diff --git a/pkg/cmd/system/info.go b/pkg/cmd/system/info.go
index 04dfe1b1df3..183fc577979 100644
--- a/pkg/cmd/system/info.go
+++ b/pkg/cmd/system/info.go
@@ -24,20 +24,22 @@ import (
"strings"
"text/template"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
+ "github.com/docker/go-units"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/containerd/containerd/api/services/introspection/v1"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/infoutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/docker/go-units"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/infoutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/logging"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
func Info(ctx context.Context, client *containerd.Client, options types.SystemInfoOptions) error {
@@ -71,6 +73,7 @@ func Info(ctx context.Context, client *containerd.Client, options types.SystemIn
if err != nil {
return err
}
+ infoCompat.Plugins.Log = logging.Drivers()
default:
return fmt.Errorf("unknown mode %q", options.Mode)
}
@@ -84,7 +87,7 @@ func Info(ctx context.Context, client *containerd.Client, options types.SystemIn
if err := tmpl.Execute(w, x); err != nil {
return err
}
- _, err = fmt.Fprintf(w, "\n")
+ _, err = fmt.Fprintln(w)
return err
}
@@ -125,12 +128,12 @@ func prettyPrintInfoNative(w io.Writer, info *native.Info) error {
}
sorter := func(x []*introspection.Plugin) func(int, int) bool {
return func(i, j int) bool {
- return x[i].Type+"."+x[j].ID < x[j].Type+"."+x[j].ID
+ return x[i].Type+"."+x[i].ID < x[j].Type+"."+x[j].ID
}
}
sort.Slice(enabledPlugins, sorter(enabledPlugins))
sort.Slice(disabledPlugins, sorter(disabledPlugins))
- fmt.Fprintf(w, "containerd Plugins:\n")
+ fmt.Fprintln(w, "containerd Plugins:")
for _, f := range enabledPlugins {
fmt.Fprintf(w, " - %s.%s\n", f.Type, f.ID)
}
@@ -147,27 +150,59 @@ func prettyPrintInfoDockerCompat(stdout io.Writer, stderr io.Writer, info *docke
fmt.Fprintf(w, "Client:\n")
fmt.Fprintf(w, " Namespace:\t%s\n", globalOptions.Namespace)
fmt.Fprintf(w, " Debug Mode:\t%v\n", debug)
- fmt.Fprintf(w, "\n")
+ fmt.Fprintln(w)
fmt.Fprintf(w, "Server:\n")
fmt.Fprintf(w, " Server Version: %s\n", info.ServerVersion)
// Storage Driver is not really Server concept for nerdctl, but mimics `docker info` output
fmt.Fprintf(w, " Storage Driver: %s\n", info.Driver)
fmt.Fprintf(w, " Logging Driver: %s\n", info.LoggingDriver)
- fmt.Fprintf(w, " Cgroup Driver: %s\n", info.CgroupDriver)
- fmt.Fprintf(w, " Cgroup Version: %s\n", info.CgroupVersion)
+ printF(w, " Cgroup Driver: ", info.CgroupDriver)
+ printF(w, " Cgroup Version: ", info.CgroupVersion)
fmt.Fprintf(w, " Plugins:\n")
- fmt.Fprintf(w, " Log: %s\n", strings.Join(info.Plugins.Log, " "))
+ fmt.Fprintf(w, " Log: %s\n", strings.Join(info.Plugins.Log, " "))
fmt.Fprintf(w, " Storage: %s\n", strings.Join(info.Plugins.Storage, " "))
+
+ // print Security options
+ printSecurityOptions(w, info.SecurityOptions)
+
+ fmt.Fprintf(w, " Kernel Version: %s\n", info.KernelVersion)
+ fmt.Fprintf(w, " Operating System: %s\n", info.OperatingSystem)
+ fmt.Fprintf(w, " OSType: %s\n", info.OSType)
+ fmt.Fprintf(w, " Architecture: %s\n", info.Architecture)
+ fmt.Fprintf(w, " CPUs: %d\n", info.NCPU)
+ fmt.Fprintf(w, " Total Memory: %s\n", units.BytesSize(float64(info.MemTotal)))
+ fmt.Fprintf(w, " Name: %s\n", info.Name)
+ fmt.Fprintf(w, " ID: %s\n", info.ID)
+
+ fmt.Fprintln(w)
+ if len(info.Warnings) > 0 {
+ fmt.Fprintln(stderr, strings.Join(info.Warnings, "\n"))
+ }
+ return nil
+}
+
+func printF(w io.Writer, label string, dockerCompatInfo string) {
+ if dockerCompatInfo == "" {
+ return
+ }
+ fmt.Fprintf(w, "%s%s\n", label, dockerCompatInfo)
+}
+
+func printSecurityOptions(w io.Writer, securityOptions []string) {
+ if len(securityOptions) == 0 {
+ return
+ }
+
fmt.Fprintf(w, " Security Options:\n")
- for _, s := range info.SecurityOptions {
+ for _, s := range securityOptions {
m, err := strutil.ParseCSVMap(s)
if err != nil {
- logrus.WithError(err).Warnf("unparsable security option %q", s)
+ log.L.WithError(err).Warnf("unparsable security option %q", s)
continue
}
name := m["name"]
if name == "" {
- logrus.Warnf("unparsable security option %q", s)
+ log.L.Warnf("unparsable security option %q", s)
continue
}
fmt.Fprintf(w, " %s\n", name)
@@ -178,18 +213,4 @@ func prettyPrintInfoDockerCompat(stdout io.Writer, stderr io.Writer, info *docke
fmt.Fprintf(w, " %s: %s\n", cases.Title(language.English).String(k), v)
}
}
- fmt.Fprintf(w, " Kernel Version: %s\n", info.KernelVersion)
- fmt.Fprintf(w, " Operating System: %s\n", info.OperatingSystem)
- fmt.Fprintf(w, " OSType: %s\n", info.OSType)
- fmt.Fprintf(w, " Architecture: %s\n", info.Architecture)
- fmt.Fprintf(w, " CPUs: %d\n", info.NCPU)
- fmt.Fprintf(w, " Total Memory: %s\n", units.BytesSize(float64(info.MemTotal)))
- fmt.Fprintf(w, " Name: %s\n", info.Name)
- fmt.Fprintf(w, " ID: %s\n", info.ID)
-
- fmt.Fprintln(w)
- if len(info.Warnings) > 0 {
- fmt.Fprintln(stderr, strings.Join(info.Warnings, "\n"))
- }
- return nil
}
diff --git a/pkg/cmd/system/prune.go b/pkg/cmd/system/prune.go
index cc3446a234e..19b48ef07c6 100644
--- a/pkg/cmd/system/prune.go
+++ b/pkg/cmd/system/prune.go
@@ -20,13 +20,14 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/cmd/builder"
- "github.com/containerd/nerdctl/pkg/cmd/container"
- "github.com/containerd/nerdctl/pkg/cmd/image"
- "github.com/containerd/nerdctl/pkg/cmd/network"
- "github.com/containerd/nerdctl/pkg/cmd/volume"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/builder"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/container"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/network"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/volume"
)
// Prune will remove all unused containers, networks,
@@ -48,6 +49,7 @@ func Prune(ctx context.Context, client *containerd.Client, options types.SystemP
if options.Volumes {
if err := volume.Prune(ctx, client, types.VolumePruneOptions{
GOptions: options.GOptions,
+ All: false,
Force: true,
Stdout: options.Stdout,
}); err != nil {
diff --git a/pkg/cmd/volume/create.go b/pkg/cmd/volume/create.go
index 791ac514b81..5aac0ce0486 100644
--- a/pkg/cmd/volume/create.go
+++ b/pkg/cmd/volume/create.go
@@ -19,23 +19,28 @@ package volume
import (
"fmt"
- "github.com/containerd/containerd/identifiers"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/strutil"
+ "github.com/docker/docker/pkg/stringid"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
-func Create(name string, options types.VolumeCreateOptions) error {
- if err := identifiers.Validate(name); err != nil {
- return fmt.Errorf("malformed name %s: %w", name, err)
+func Create(name string, options types.VolumeCreateOptions) (*native.Volume, error) {
+ if name == "" {
+ name = stringid.GenerateRandomID()
+ options.Labels = append(options.Labels, labels.AnonymousVolumes+"=")
}
volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
if err != nil {
- return err
+ return nil, err
}
labels := strutil.DedupeStrSlice(options.Labels)
- if _, err := volStore.Create(name, labels); err != nil {
- return err
+ vol, err := volStore.Create(name, labels)
+ if err != nil {
+ return nil, err
}
- fmt.Fprintf(options.Stdout, "%s\n", name)
- return nil
+ fmt.Fprintln(options.Stdout, name)
+ return vol, nil
}
diff --git a/pkg/cmd/volume/inspect.go b/pkg/cmd/volume/inspect.go
index 78221c6bd56..8369a6c9ea8 100644
--- a/pkg/cmd/volume/inspect.go
+++ b/pkg/cmd/volume/inspect.go
@@ -17,23 +17,41 @@
package volume
import (
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
+ "context"
+ "errors"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
)
-func Inspect(volumes []string, options types.VolumeInspectOptions) error {
+func Inspect(ctx context.Context, volumes []string, options types.VolumeInspectOptions) error {
volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
if err != nil {
return err
}
- result := make([]interface{}, len(volumes))
+ result := []interface{}{}
- for i, name := range volumes {
+ warns := []error{}
+ for _, name := range volumes {
var vol, err = volStore.Get(name, options.Size)
if err != nil {
- return err
+ warns = append(warns, err)
+ continue
}
- result[i] = vol
+ result = append(result, vol)
+ }
+ err = formatter.FormatSlice(options.Format, options.Stdout, result)
+ if err != nil {
+ return err
+ }
+ for _, warn := range warns {
+ log.G(ctx).Warn(warn)
+ }
+
+ if len(warns) != 0 {
+ return errors.New("some volumes could not be inspected")
}
- return formatter.FormatSlice(options.Format, options.Stdout, result)
+ return nil
}
diff --git a/pkg/cmd/volume/list.go b/pkg/cmd/volume/list.go
index a026af5642c..bb0654ba5b2 100644
--- a/pkg/cmd/volume/list.go
+++ b/pkg/cmd/volume/list.go
@@ -25,11 +25,12 @@ import (
"text/tabwriter"
"text/template"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/pkg/progress"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
type volumePrintable struct {
@@ -44,16 +45,16 @@ type volumePrintable struct {
func List(options types.VolumeListOptions) error {
if options.Quiet && options.Size {
- logrus.Warn("cannot use --size and --quiet together, ignoring --size")
+ log.L.Warn("cannot use --size and --quiet together, ignoring --size")
options.Size = false
}
sizeFilter := hasSizeFilter(options.Filters)
if sizeFilter && options.Quiet {
- logrus.Warn("cannot use --filter=size and --quiet together, ignoring --filter=size")
+ log.L.Warn("cannot use --filter=size and --quiet together, ignoring --filter=size")
options.Filters = removeSizeFilters(options.Filters)
}
if sizeFilter && !options.Size {
- logrus.Warn("should use --filter=size and --size together")
+ log.L.Warn("should use --filter=size and --size together")
options.Size = true
}
@@ -134,7 +135,7 @@ func lsPrintOutput(vols map[string]native.Volume, options types.VolumeListOption
if err := tmpl.Execute(&b, p); err != nil {
return err
}
- if _, err := fmt.Fprintf(w, b.String()+"\n"); err != nil {
+ if _, err := fmt.Fprintln(w, b.String()); err != nil {
return err
}
} else if options.Quiet {
@@ -268,15 +269,28 @@ func volumeMatchesFilter(vol native.Volume, labelFilterFuncs []func(*map[string]
return false
}
}
- for _, nameFilterFunc := range nameFilterFuncs {
- if !nameFilterFunc(vol.Name) {
- return false
- }
+
+ if !anyMatch(vol.Name, nameFilterFuncs) {
+ return false
}
+
for _, sizeFilterFunc := range sizeFilterFuncs {
if !sizeFilterFunc(vol.Size) {
return false
}
}
+
return true
}
+
+func anyMatch[T any](vol T, filters []func(T) bool) bool {
+ if len(filters) == 0 {
+ return true
+ }
+ for _, f := range filters {
+ if f(vol) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/pkg/cmd/volume/prune.go b/pkg/cmd/volume/prune.go
index 06cd202e955..71e6d20e779 100644
--- a/pkg/cmd/volume/prune.go
+++ b/pkg/cmd/volume/prune.go
@@ -19,46 +19,66 @@ package volume
import (
"context"
"fmt"
+ "strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func Prune(ctx context.Context, client *containerd.Client, options types.VolumePruneOptions) error {
+ // Get the volume store and lock it until we are done.
+ // This will prevent racing new containers from being created or removed until we are done with the cleanup of volumes
volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
if err != nil {
return err
}
- volumes, err := volStore.List(false)
- if err != nil {
- return err
- }
- containers, err := client.Containers(ctx)
- if err != nil {
- return err
- }
- usedVolumes, err := usedVolumes(ctx, containers)
- if err != nil {
- return err
- }
- var removeNames []string // nolint: prealloc
- for _, volume := range volumes {
- if _, ok := usedVolumes[volume.Name]; ok {
- continue
+ var toRemove []string // nolint: prealloc
+
+ err = volStore.Prune(func(volumes []*native.Volume) ([]string, error) {
+ // Get containers and see which volumes are used
+ containers, err := client.Containers(ctx)
+ if err != nil {
+ return nil, err
}
- removeNames = append(removeNames, volume.Name)
- }
- removedNames, err := volStore.Remove(removeNames)
+
+ usedVolumesList, err := usedVolumes(ctx, containers)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, volume := range volumes {
+ if _, ok := usedVolumesList[volume.Name]; ok {
+ continue
+ }
+ if !options.All {
+ if volume.Labels == nil {
+ continue
+ }
+ val, ok := (*volume.Labels)[labels.AnonymousVolumes]
+ // skip the named volume and only remove the anonymous volume
+ if !ok || val != "" {
+ continue
+ }
+ }
+ toRemove = append(toRemove, volume.Name)
+ }
+
+ return toRemove, nil
+ })
+
if err != nil {
return err
}
- if len(removedNames) > 0 {
+
+ if len(toRemove) > 0 {
fmt.Fprintln(options.Stdout, "Deleted Volumes:")
- for _, name := range removedNames {
- fmt.Fprintln(options.Stdout, name)
- }
+ fmt.Fprintln(options.Stdout, strings.Join(toRemove, "\n"))
fmt.Fprintln(options.Stdout, "")
}
+
return nil
}
diff --git a/pkg/cmd/volume/rm.go b/pkg/cmd/volume/rm.go
index 98a5a44c4bb..4b01564c009 100644
--- a/pkg/cmd/volume/rm.go
+++ b/pkg/cmd/volume/rm.go
@@ -19,55 +19,77 @@ package volume
import (
"context"
"encoding/json"
+ "errors"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/mountutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil"
)
func Remove(ctx context.Context, client *containerd.Client, volumes []string, options types.VolumeRemoveOptions) error {
- containers, err := client.Containers(ctx)
- if err != nil {
- return err
- }
volStore, err := Store(options.GOptions.Namespace, options.GOptions.DataRoot, options.GOptions.Address)
if err != nil {
return err
}
- usedVolumes, err := usedVolumes(ctx, containers)
+
+ containers, err := client.Containers(ctx)
if err != nil {
return err
}
- var volumenames []string // nolint: prealloc
- for _, name := range volumes {
- volume, err := volStore.Get(name, false)
+ // Note: to avoid racy behavior, this is called by volStore.Remove *inside a lock*
+ removableVolumes := func() (volumeNames []string, cannotRemove []error, err error) {
+ usedVolumesList, err := usedVolumes(ctx, containers)
if err != nil {
- return err
+ return nil, nil, err
}
- if _, ok := usedVolumes[volume.Name]; ok {
- return fmt.Errorf("volume %q is in use", name)
+
+ for _, name := range volumes {
+ if _, ok := usedVolumesList[name]; ok {
+ cannotRemove = append(cannotRemove, fmt.Errorf("volume %q is in use (%w)", name, errdefs.ErrFailedPrecondition))
+ continue
+ }
+ volumeNames = append(volumeNames, name)
}
- volumenames = append(volumenames, name)
+
+ return volumeNames, cannotRemove, nil
}
- removedNames, err := volStore.Remove(volumenames)
+
+ removedNames, cannotRemove, err := volStore.Remove(removableVolumes)
if err != nil {
return err
}
+ // Otherwise, output on stdout whatever was successful
for _, name := range removedNames {
fmt.Fprintln(options.Stdout, name)
}
- return err
+ // Log the rest
+ for _, volErr := range cannotRemove {
+ log.G(ctx).Warn(volErr)
+ }
+ if len(cannotRemove) > 0 {
+ return errors.New("some volumes could not be removed")
+ }
+ return nil
}
func usedVolumes(ctx context.Context, containers []containerd.Container) (map[string]struct{}, error) {
- usedVolumes := make(map[string]struct{})
+ usedVolumesList := make(map[string]struct{})
for _, c := range containers {
l, err := c.Labels(ctx)
if err != nil {
+ // Containerd note: there is no guarantee that the containers we got from the list still exist at this point
+ // If that is the case, just ignore and move on
+ if errors.Is(err, errdefs.ErrNotFound) {
+ log.G(ctx).Debugf("container %q is gone - ignoring", c.ID())
+ continue
+ }
return nil, err
}
mountsJSON, ok := l[labels.Mounts]
@@ -82,9 +104,9 @@ func usedVolumes(ctx context.Context, containers []containerd.Container) (map[st
}
for _, m := range mounts {
if m.Type == mountutil.Volume {
- usedVolumes[m.Name] = struct{}{}
+ usedVolumesList[m.Name] = struct{}{}
}
}
}
- return usedVolumes, nil
+ return usedVolumesList, nil
}
diff --git a/pkg/cmd/volume/volume.go b/pkg/cmd/volume/volume.go
index 6efb1a7b46b..1a6fc9f16f1 100644
--- a/pkg/cmd/volume/volume.go
+++ b/pkg/cmd/volume/volume.go
@@ -17,8 +17,8 @@
package volume
import (
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
)
// Store returns a volume store
diff --git a/pkg/composer/build.go b/pkg/composer/build.go
index d389c358157..17b3fd0d8cd 100644
--- a/pkg/composer/build.go
+++ b/pkg/composer/build.go
@@ -21,10 +21,11 @@ import (
"fmt"
"os"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
)
type BuildOptions struct {
@@ -34,8 +35,8 @@ type BuildOptions struct {
}
func (c *Composer) Build(ctx context.Context, bo BuildOptions, services []string) error {
- return c.project.WithServices(services, func(svc types.ServiceConfig) error {
- ps, err := serviceparser.Parse(c.project, svc)
+ return c.project.ForEachService(services, func(names string, svc *types.ServiceConfig) error {
+ ps, err := serviceparser.Parse(c.project, *svc)
if err != nil {
return err
}
@@ -47,7 +48,7 @@ func (c *Composer) Build(ctx context.Context, bo BuildOptions, services []string
}
func (c *Composer) buildServiceImage(ctx context.Context, image string, b *serviceparser.Build, platform string, bo BuildOptions) error {
- logrus.Infof("Building image %s", image)
+ log.G(ctx).Infof("Building image %s", image)
var args []string // nolint: prealloc
if platform != "" {
@@ -66,7 +67,7 @@ func (c *Composer) buildServiceImage(ctx context.Context, image string, b *servi
cmd := c.createNerdctlCmd(ctx, append([]string{"build"}, args...)...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
diff --git a/pkg/composer/composer.go b/pkg/composer/composer.go
index ef60632d96d..d159fbbbfaf 100644
--- a/pkg/composer/composer.go
+++ b/pkg/composer/composer.go
@@ -23,13 +23,15 @@ import (
"fmt"
"os/exec"
- composecli "github.com/compose-spec/compose-go/cli"
- compose "github.com/compose-spec/compose-go/types"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/identifiers"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/reflectutil"
- "github.com/sirupsen/logrus"
+ composecli "github.com/compose-spec/compose-go/v2/cli"
+ compose "github.com/compose-spec/compose-go/v2/types"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/identifiers"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
// Options groups the command line options recommended for a Compose implementation (ProjectOptions) and extra options for nerdctl
@@ -61,8 +63,8 @@ func New(o Options, client *containerd.Client) (*Composer, error) {
}
if o.Project != "" {
- if err := identifiers.Validate(o.Project); err != nil {
- return nil, fmt.Errorf("got invalid project name %q: %w", o.Project, err)
+ if err := identifiers.ValidateDockerCompat(o.Project); err != nil {
+ return nil, fmt.Errorf("invalid project name: %w", err)
}
}
@@ -70,9 +72,16 @@ func New(o Options, client *containerd.Client) (*Composer, error) {
optionsFn = append(optionsFn,
composecli.WithOsEnv,
composecli.WithWorkingDirectory(o.ProjectDirectory),
- composecli.WithEnvFile(o.EnvFile),
+ )
+ if o.EnvFile != "" {
+ optionsFn = append(optionsFn,
+ composecli.WithEnvFiles(o.EnvFile),
+ )
+ }
+ optionsFn = append(optionsFn,
composecli.WithConfigFileEnv,
composecli.WithDefaultConfigPath,
+ composecli.WithEnvFiles(),
composecli.WithDotEnv,
composecli.WithName(o.Project),
)
@@ -81,7 +90,7 @@ func New(o Options, client *containerd.Client) (*Composer, error) {
if err != nil {
return nil, err
}
- project, err := composecli.ProjectFromOptions(projectOptions)
+ project, err := projectOptions.LoadProject(context.TODO())
if err != nil {
return nil, err
}
@@ -93,12 +102,16 @@ func New(o Options, client *containerd.Client) (*Composer, error) {
}
o.Profiles = append(o.Profiles, s.GetProfiles()...)
}
- project.ApplyProfiles(o.Profiles)
+
+ project, err = project.WithProfiles(o.Profiles)
+ if err != nil {
+ return nil, err
+ }
if o.DebugPrintFull {
projectJSON, _ := json.MarshalIndent(project, "", " ")
- logrus.Debug("printing project JSON")
- logrus.Debugf("%s", projectJSON)
+ log.L.Debug("printing project JSON")
+ log.L.Debugf("%s", projectJSON)
}
if unknown := reflectutil.UnknownNonEmptyFields(project,
@@ -111,7 +124,7 @@ func New(o Options, client *containerd.Client) (*Composer, error) {
"Secrets",
"Configs",
"ComposeFiles"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: %+v", unknown)
+ log.L.Warnf("Ignoring: %+v", unknown)
}
c := &Composer{
@@ -136,7 +149,7 @@ func (c *Composer) createNerdctlCmd(ctx context.Context, args ...string) *exec.C
func (c *Composer) runNerdctlCmd(ctx context.Context, args ...string) error {
cmd := c.createNerdctlCmd(ctx, args...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("error while executing %v: %q: %w", cmd.Args, string(out), err)
@@ -147,8 +160,9 @@ func (c *Composer) runNerdctlCmd(ctx context.Context, args ...string) error {
// Services returns the parsed Service objects in dependency order.
func (c *Composer) Services(ctx context.Context, svcs ...string) ([]*serviceparser.Service, error) {
var services []*serviceparser.Service
- if err := c.project.WithServices(svcs, func(svc compose.ServiceConfig) error {
- parsed, err := serviceparser.Parse(c.project, svc)
+
+ if err := c.project.ForEachService(svcs, func(name string, svc *compose.ServiceConfig) error {
+ parsed, err := serviceparser.Parse(c.project, *svc)
if err != nil {
return err
}
@@ -163,7 +177,7 @@ func (c *Composer) Services(ctx context.Context, svcs ...string) ([]*servicepars
// ServiceNames returns service names in dependency order.
func (c *Composer) ServiceNames(svcs ...string) ([]string, error) {
var names []string
- if err := c.project.WithServices(svcs, func(svc compose.ServiceConfig) error {
+ if err := c.project.ForEachService(svcs, func(name string, svc *compose.ServiceConfig) error {
names = append(names, svc.Name)
return nil
}); err != nil {
diff --git a/pkg/composer/config.go b/pkg/composer/config.go
index c58f67326ad..41a5320daf8 100644
--- a/pkg/composer/config.go
+++ b/pkg/composer/config.go
@@ -30,7 +30,7 @@ import (
"io"
"strings"
- "github.com/compose-spec/compose-go/types"
+ "github.com/compose-spec/compose-go/v2/types"
"github.com/opencontainers/go-digest"
"gopkg.in/yaml.v3"
)
@@ -59,17 +59,14 @@ func (c *Composer) Config(ctx context.Context, w io.Writer, co ConfigOptions) er
if co.Hash != "*" {
services = strings.Split(co.Hash, ",")
}
- if err := c.project.WithServices(services, func(svc types.ServiceConfig) error {
- hash, err := ServiceHash(svc)
+ return c.project.ForEachService(services, func(names string, svc *types.ServiceConfig) error {
+ hash, err := ServiceHash(*svc)
if err != nil {
return err
}
- fmt.Fprintf(w, "%s %s\n", svc.Name, hash)
- return nil
- }); err != nil {
+ _, err = fmt.Fprintf(w, "%s %s\n", svc.Name, hash)
return err
- }
- return nil
+ })
}
projectYAML, err := yaml.Marshal(c.project)
if err != nil {
@@ -84,7 +81,8 @@ func ServiceHash(o types.ServiceConfig) (string, error) {
// remove the Build config when generating the service hash
o.Build = nil
o.PullPolicy = ""
- o.Scale = 1
+ o.Scale = new(int)
+ *(o.Scale) = 1
bytes, err := json.Marshal(o)
if err != nil {
return "", err
diff --git a/pkg/composer/container.go b/pkg/composer/container.go
index 3e5f94e9c6d..c789e42696a 100644
--- a/pkg/composer/container.go
+++ b/pkg/composer/container.go
@@ -20,9 +20,10 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func (c *Composer) Containers(ctx context.Context, services ...string) ([]containerd.Container, error) {
@@ -34,7 +35,7 @@ func (c *Composer) Containers(ctx context.Context, services ...string) ([]contai
if len(services) == 0 {
filters = append(filters, projectLabel)
}
- logrus.Debugf("filters: %v", filters)
+ log.G(ctx).Debugf("filters: %v", filters)
containers, err := c.client.Containers(ctx, filters...)
if err != nil {
return nil, err
@@ -62,3 +63,24 @@ func (c *Composer) containerExists(ctx context.Context, name, service string) (b
// container doesn't exist
return false, nil
}
+
+func (c *Composer) containerID(ctx context.Context, name, service string) (string, error) {
+ // get list of containers for service
+ containers, err := c.Containers(ctx, service)
+ if err != nil {
+ return "", err
+ }
+
+ for _, container := range containers {
+ containerLabels, err := container.Labels(ctx)
+ if err != nil {
+ return "", err
+ }
+ if name == containerLabels[labels.Name] {
+ // container exists
+ return container.ID(), nil
+ }
+ }
+ // container doesn't exist
+ return "", nil
+}
diff --git a/pkg/composer/copy.go b/pkg/composer/copy.go
new file mode 100644
index 00000000000..a75d56bac33
--- /dev/null
+++ b/pkg/composer/copy.go
@@ -0,0 +1,163 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package composer
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/docker/docker/pkg/system"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+)
+
+type CopyOptions struct {
+ Source string
+ Destination string
+ Index int
+ FollowLink bool
+ DryRun bool
+}
+
+type copyDirection int
+
+const (
+ fromService copyDirection = 0
+ toService copyDirection = 1
+)
+
+func (c *Composer) Copy(ctx context.Context, co CopyOptions) error {
+ srcService, srcPath := splitCpArg(co.Source)
+ destService, dstPath := splitCpArg(co.Destination)
+ var serviceName string
+ var direction copyDirection
+
+ if srcService != "" && destService != "" {
+ return errors.New("copying between services is not supported")
+ }
+ if srcService == "" && destService == "" {
+ return errors.New("unknown copy direction")
+ }
+
+ if srcService != "" {
+ direction = fromService
+ serviceName = srcService
+ }
+ if destService != "" {
+ direction = toService
+ serviceName = destService
+ }
+
+ containers, err := c.listContainersTargetedForCopy(ctx, co.Index, direction, serviceName)
+ if err != nil {
+ return err
+ }
+
+ for _, container := range containers {
+ args := []string{"cp"}
+ if co.FollowLink {
+ args = append(args, "--follow-link")
+ }
+ if direction == fromService {
+ args = append(args, fmt.Sprintf("%s:%s", container.ID(), srcPath), dstPath)
+ }
+ if direction == toService {
+ args = append(args, srcPath, fmt.Sprintf("%s:%s", container.ID(), dstPath))
+ }
+ err := c.logCopyMsg(ctx, container, direction, srcService, srcPath, destService, dstPath, co.DryRun)
+ if err != nil {
+ return err
+ }
+ if !co.DryRun {
+ if err := c.runNerdctlCmd(ctx, args...); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+func (c *Composer) logCopyMsg(ctx context.Context, container containerd.Container, direction copyDirection, srcService string, srcPath string, destService string, dstPath string, dryRun bool) error {
+ containerLabels, err := container.Labels(ctx)
+ if err != nil {
+ return err
+ }
+ containerName := containerLabels[labels.Name]
+ msg := ""
+ if dryRun {
+ msg = "DRY-RUN MODE - "
+ }
+ if direction == fromService {
+ msg = msg + fmt.Sprintf("copy %s:%s to %s", containerName, srcPath, dstPath)
+ }
+ if direction == toService {
+ msg = msg + fmt.Sprintf("copy %s to %s:%s", srcPath, containerName, dstPath)
+ }
+ log.G(ctx).Info(msg)
+ return nil
+}
+
+func (c *Composer) listContainersTargetedForCopy(ctx context.Context, index int, direction copyDirection, serviceName string) ([]containerd.Container, error) {
+ var containers []containerd.Container
+ var err error
+
+ containers, err = c.Containers(ctx, serviceName)
+ if err != nil {
+ return nil, err
+ }
+
+ if index > 0 {
+ if index > len(containers) {
+ return nil, fmt.Errorf("index (%d) out of range: only %d running instances from service %s",
+ index, len(containers), serviceName)
+ }
+ container := containers[index-1]
+ return []containerd.Container{container}, nil
+ }
+
+ if len(containers) < 1 {
+ return nil, fmt.Errorf("no container found for service %q", serviceName)
+ }
+ if direction == fromService {
+ return containers[:1], err
+
+ }
+ return containers, err
+}
+
+// https://github.com/docker/compose/blob/v2.21.0/pkg/compose/cp.go#L307
+func splitCpArg(arg string) (container, path string) {
+ if system.IsAbs(arg) {
+ // Explicit local absolute path, e.g., `C:\foo` or `/foo`.
+ return "", arg
+ }
+
+ parts := strings.SplitN(arg, ":", 2)
+
+ if len(parts) == 1 || strings.HasPrefix(parts[0], ".") {
+ // Either there's no `:` in the arg
+ // OR it's an explicit local relative path like `./file:name.txt`.
+ return "", arg
+ }
+
+ return parts[0], parts[1]
+}
diff --git a/pkg/composer/create.go b/pkg/composer/create.go
index a7aff1f5482..a2ae9180160 100644
--- a/pkg/composer/create.go
+++ b/pkg/composer/create.go
@@ -23,11 +23,13 @@ import (
"path/filepath"
"strings"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/sirupsen/logrus"
+ "github.com/compose-spec/compose-go/v2/types"
"golang.org/x/sync/errgroup"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
// FYI: https://github.com/docker/compose/blob/v2.14.1/pkg/api/api.go#L423
@@ -117,7 +119,7 @@ func (c *Composer) Create(ctx context.Context, opt CreateOptions, services []str
return err
}
for _, ps := range parsedServices {
- if err := c.ensureServiceImage(ctx, ps, !opt.NoBuild, opt.Build, BuildOptions{}, false); err != nil {
+ if err := c.ensureServiceImage(ctx, ps, !opt.NoBuild, opt.Build, BuildOptions{}, false, ""); err != nil {
return err
}
}
@@ -144,10 +146,7 @@ func (c *Composer) createService(ctx context.Context, ps *serviceparser.Service,
return nil
})
}
- if err := runEG.Wait(); err != nil {
- return err
- }
- return nil
+ return runEG.Wait()
}
// createServiceContainer must be called after ensureServiceImage
@@ -166,18 +165,18 @@ func (c *Composer) createServiceContainer(ctx context.Context, service *servicep
// delete container if it already exists and force-recreate is enabled
if exists {
if recreate != RecreateForce {
- logrus.Infof("Container %s exists, skipping", container.Name)
+ log.G(ctx).Infof("Container %s exists, skipping", container.Name)
return "", nil
}
- logrus.Debugf("Container %q already exists and force-created is enabled, deleting", container.Name)
+ log.G(ctx).Debugf("Container %q already exists and force-created is enabled, deleting", container.Name)
delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name)
if err = delCmd.Run(); err != nil {
return "", fmt.Errorf("could not delete container %q: %s", container.Name, err)
}
- logrus.Infof("Re-creating container %s", container.Name)
+ log.G(ctx).Infof("Re-creating container %s", container.Name)
} else {
- logrus.Infof("Creating container %s", container.Name)
+ log.G(ctx).Infof("Creating container %s", container.Name)
}
tempDir, err := os.MkdirTemp(os.TempDir(), "compose-")
@@ -196,7 +195,7 @@ func (c *Composer) createServiceContainer(ctx context.Context, service *servicep
cmd := c.createNerdctlCmd(ctx, append([]string{"create"}, container.RunArgs...)...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
// FIXME
diff --git a/pkg/composer/down.go b/pkg/composer/down.go
index 9a7a7cc33b7..d02cf613082 100644
--- a/pkg/composer/down.go
+++ b/pkg/composer/down.go
@@ -20,9 +20,9 @@ import (
"context"
"fmt"
- "github.com/containerd/nerdctl/pkg/strutil"
+ "github.com/containerd/log"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
type DownOptions struct {
@@ -41,6 +41,10 @@ func (c *Composer) Down(ctx context.Context, downOptions DownOptions) error {
if err != nil {
return err
}
+ // use default Options to stop service containers.
+ if err := c.stopContainers(ctx, containers, StopOptions{}); err != nil {
+ return err
+ }
if err := c.removeContainers(ctx, containers, RemoveOptions{Stop: true, Volumes: downOptions.RemoveVolumes}); err != nil {
return err
}
@@ -61,7 +65,7 @@ func (c *Composer) Down(ctx context.Context, downOptions DownOptions) error {
return fmt.Errorf("error removeing orphaned containers: %s", err)
}
} else {
- logrus.Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
+ log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
}
}
@@ -87,7 +91,7 @@ func (c *Composer) downNetwork(ctx context.Context, shortName string) error {
if !ok {
return fmt.Errorf("invalid network name %q", shortName)
}
- if net.External.External {
+ if net.External {
// NOP
return nil
}
@@ -105,9 +109,9 @@ func (c *Composer) downNetwork(ctx context.Context, shortName string) error {
return fmt.Errorf("network %s is in use", fullName)
}
- logrus.Infof("Removing network %s", fullName)
+ log.G(ctx).Infof("Removing network %s", fullName)
if err := c.runNerdctlCmd(ctx, "network", "rm", fullName); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}
return nil
@@ -118,19 +122,20 @@ func (c *Composer) downVolume(ctx context.Context, shortName string) error {
if !ok {
return fmt.Errorf("invalid volume name %q", shortName)
}
- if vol.External.External {
+ if vol.External {
// NOP
return nil
}
// shortName is like "db_data", fullName is like "compose-wordpress_db_data"
fullName := vol.Name
+ // FIXME: this is racy. See note in up_volume.go
volExists, err := c.VolumeExists(fullName)
if err != nil {
return err
} else if volExists {
- logrus.Infof("Removing volume %s", fullName)
+ log.G(ctx).Infof("Removing volume %s", fullName)
if err := c.runNerdctlCmd(ctx, "volume", "rm", "-f", fullName); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}
return nil
diff --git a/pkg/composer/exec.go b/pkg/composer/exec.go
index 7b267365e18..04c6e077614 100644
--- a/pkg/composer/exec.go
+++ b/pkg/composer/exec.go
@@ -20,9 +20,15 @@ import (
"context"
"fmt"
"os"
+ "sort"
+ "strconv"
+ "strings"
- "github.com/containerd/containerd"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
// ExecOptions stores options passed from users as flags and args.
@@ -54,9 +60,21 @@ func (c *Composer) Exec(ctx context.Context, eo ExecOptions) error {
return fmt.Errorf("index (%d) out of range: only %d running instances from service %s",
eo.Index, len(containers), eo.ServiceName)
}
- container := containers[eo.Index-1]
-
- return c.exec(ctx, container, eo)
+ if len(containers) == 1 {
+ return c.exec(ctx, containers[0], eo)
+ }
+ // The order of the containers is not consistently ascending
+ // we need to re-sort them.
+ sort.SliceStable(containers, func(i, j int) bool {
+ infoI, _ := containers[i].Info(ctx, containerd.WithoutRefreshedMetadata)
+ infoJ, _ := containers[j].Info(ctx, containerd.WithoutRefreshedMetadata)
+ segsI := strings.Split(infoI.Labels[labels.Name], serviceparser.Separator)
+ segsJ := strings.Split(infoJ.Labels[labels.Name], serviceparser.Separator)
+ indexI, _ := strconv.Atoi(segsI[len(segsI)-1])
+ indexJ, _ := strconv.Atoi(segsJ[len(segsJ)-1])
+ return indexI < indexJ
+ })
+ return c.exec(ctx, containers[eo.Index-1], eo)
}
// exec constructs/executes the `nerdctl exec` command to be executed on the given container.
@@ -90,7 +108,7 @@ func (c *Composer) exec(ctx context.Context, container containerd.Container, eo
}
if c.DebugPrintFull {
- logrus.Debugf("Executing %v", cmd.Args)
+ log.G(ctx).Debugf("Executing %v", cmd.Args)
}
return cmd.Run()
}
diff --git a/pkg/composer/kill.go b/pkg/composer/kill.go
index fc63a751d4f..204ad30690a 100644
--- a/pkg/composer/kill.go
+++ b/pkg/composer/kill.go
@@ -19,8 +19,9 @@ package composer
import (
"context"
- "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
+
+ "github.com/containerd/log"
)
type KillOptions struct {
@@ -42,7 +43,7 @@ func (c *Composer) Kill(ctx context.Context, opts KillOptions, services []string
eg.Go(func() error {
args := []string{"kill", "-s", opts.Signal, container.ID()}
if err := c.runNerdctlCmd(ctx, args...); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
return err
}
return nil
diff --git a/pkg/composer/lock.go b/pkg/composer/lock.go
new file mode 100644
index 00000000000..8fedda7bfc4
--- /dev/null
+++ b/pkg/composer/lock.go
@@ -0,0 +1,48 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package composer
+
+import (
+ "os"
+
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/lockutil"
+)
+
+//nolint:unused
+var locked *os.File
+
+func Lock(dataRoot string, address string) error {
+ // Compose right now cannot be made safe to use concurrently, as we shell out to nerdctl for multiple operations,
+ // preventing us from using the lock mechanisms from the API.
+ // This here allows to impose a global lock, effectively preventing multiple compose commands from being run in parallel and
+ // preventing some of the problems with concurrent execution.
+ // This should be removed once we have better, in-depth solutions to make compose concurrency safe.
+ // Note that in most cases we do not close the lock explicitly. Instead, the lock will get released when the `locked` global
+ // variable will get collected and the file descriptor closed (eg: when the binary exits).
+ var err error
+ dataStore, err := clientutil.DataStore(dataRoot, address)
+ if err != nil {
+ return err
+ }
+ locked, err = lockutil.Lock(dataStore)
+ return err
+}
+
+func Unlock() error {
+ return lockutil.Unlock(locked)
+}
diff --git a/pkg/composer/logs.go b/pkg/composer/logs.go
index 23e4ee94e7c..3d6f5cc2f65 100644
--- a/pkg/composer/logs.go
+++ b/pkg/composer/logs.go
@@ -18,31 +18,43 @@ package composer
import (
"context"
+ "fmt"
"os"
"os/exec"
"os/signal"
"strings"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/composer/pipetagger"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/labels"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/pipetagger"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
type LogsOptions struct {
- Follow bool
- Timestamps bool
- Tail string
- NoColor bool
- NoLogPrefix bool
+ AbortOnContainerExit bool
+ Follow bool
+ Timestamps bool
+ Tail string
+ NoColor bool
+ NoLogPrefix bool
+ LatestRun bool
}
func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string) error {
+ // Whether we called `compose logs`, or we are showing logs at the end of `up`, while in non detach mode, we need
+ // to release the lock. At this point, no operation will be performed that needs exclusive locking anymore, and
+ // not releasing the lock would otherwise unduly prevent further compose operations.
+ // See https://github.com/containerd/nerdctl/issues/3678
+ if err := Unlock(); err != nil {
+ return err
+ }
+
var serviceNames []string
- err := c.project.WithServices(services, func(svc types.ServiceConfig) error {
+ err := c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error {
serviceNames = append(serviceNames, svc.Name)
return nil
}, types.IgnoreDependencies)
@@ -59,9 +71,10 @@ func (c *Composer) Logs(ctx context.Context, lo LogsOptions, services []string)
func (c *Composer) logs(ctx context.Context, containers []containerd.Container, lo LogsOptions) error {
var logTagMaxLen int
type containerState struct {
- name string
- logTag string
- logCmd *exec.Cmd
+ name string
+ logTag string
+ logCmd *exec.Cmd
+ startedAt string
}
containerStates := make(map[string]containerState, len(containers)) // key: containerID
@@ -75,9 +88,15 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container,
if l := len(logTag); l > logTagMaxLen {
logTagMaxLen = l
}
+ ts, err := info.UpdatedAt.MarshalText()
+ if err != nil {
+ return err
+ }
+
containerStates[container.ID()] = containerState{
- name: name,
- logTag: logTag,
+ name: name,
+ logTag: logTag,
+ startedAt: string(ts),
}
}
@@ -99,6 +118,9 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container,
args = append(args, lo.Tail)
}
}
+ if lo.LatestRun {
+ args = append(args, fmt.Sprintf("--since=%s", state.startedAt))
+ }
args = append(args, id)
state.logCmd = c.createNerdctlCmd(ctx, args...)
@@ -117,7 +139,7 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container,
}
stderrTagger := pipetagger.New(os.Stderr, stderr, state.logTag, logWidth, lo.NoColor)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", state.logCmd.Args)
+ log.G(ctx).Debugf("Running %v", state.logCmd.Args)
}
if err := state.logCmd.Start(); err != nil {
return err
@@ -134,26 +156,33 @@ func (c *Composer) logs(ctx context.Context, containers []containerd.Container,
signal.Notify(interruptChan, os.Interrupt)
logsEOFMap := make(map[string]struct{}) // key: container name
+ var containerError error
selectLoop:
for {
// Wait for Ctrl-C, or `nerdctl compose down` in another terminal
select {
case sig := <-interruptChan:
- logrus.Debugf("Received signal: %s", sig)
+ log.G(ctx).Debugf("Received signal: %s", sig)
break selectLoop
case containerName := <-logsEOFChan:
if lo.Follow {
// When `nerdctl logs -f` has exited, we can assume that the container has exited
- logrus.Infof("Container %q exited", containerName)
+ log.G(ctx).Infof("Container %q exited", containerName)
+ // In case a container has exited and the parameter --abort-on-container-exit,
+ // we break the loop and set an error, so we can exit the program with 1
+ if lo.AbortOnContainerExit {
+ containerError = fmt.Errorf("container %q exited", containerName)
+ break selectLoop
+ }
} else {
- logrus.Debugf("Logs for container %q reached EOF", containerName)
+ log.G(ctx).Debugf("Logs for container %q reached EOF", containerName)
}
logsEOFMap[containerName] = struct{}{}
if len(logsEOFMap) == len(containerStates) {
if lo.Follow {
- logrus.Info("All the containers have exited")
+ log.G(ctx).Info("All the containers have exited")
} else {
- logrus.Debug("All the logs reached EOF")
+ log.G(ctx).Debug("All the logs reached EOF")
}
break selectLoop
}
@@ -163,10 +192,10 @@ selectLoop:
for _, state := range containerStates {
if state.logCmd != nil && state.logCmd.Process != nil {
if err := state.logCmd.Process.Kill(); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}
}
- return nil
+ return containerError
}
diff --git a/pkg/composer/orphans.go b/pkg/composer/orphans.go
index 2466d4ca174..eacac677fc3 100644
--- a/pkg/composer/orphans.go
+++ b/pkg/composer/orphans.go
@@ -20,9 +20,10 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/labels"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func (c *Composer) getOrphanContainers(ctx context.Context, parsedServices []*serviceparser.Service) ([]containerd.Container, error) {
diff --git a/pkg/composer/pause.go b/pkg/composer/pause.go
index 6f94cd60ffb..d0e7bc5aa77 100644
--- a/pkg/composer/pause.go
+++ b/pkg/composer/pause.go
@@ -22,10 +22,12 @@ import (
"io"
"sync"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/containerutil"
- "github.com/containerd/nerdctl/pkg/labels"
"golang.org/x/sync/errgroup"
+
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
// Pause pauses service containers belonging to `services`.
@@ -55,7 +57,7 @@ func (c *Composer) Pause(ctx context.Context, services []string, writer io.Write
mu.Lock()
defer mu.Unlock()
- _, err = fmt.Fprintf(writer, "%s\n", info.Labels[labels.Name])
+ _, err = fmt.Fprintln(writer, info.Labels[labels.Name])
return err
})
@@ -91,7 +93,7 @@ func (c *Composer) Unpause(ctx context.Context, services []string, writer io.Wri
mu.Lock()
defer mu.Unlock()
- _, err = fmt.Fprintf(writer, "%s\n", info.Labels[labels.Name])
+ _, err = fmt.Fprintln(writer, info.Labels[labels.Name])
return err
})
diff --git a/pkg/composer/pipetagger/pipetagger.go b/pkg/composer/pipetagger/pipetagger.go
index b9f485b80cd..9b014354463 100644
--- a/pkg/composer/pipetagger/pipetagger.go
+++ b/pkg/composer/pipetagger/pipetagger.go
@@ -107,8 +107,5 @@ func (x *PipeTagger) Run() error {
)
}
}
- if err := scanner.Err(); err != nil {
- return err
- }
- return nil
+ return scanner.Err()
}
diff --git a/pkg/composer/port.go b/pkg/composer/port.go
index 12676eb981f..750ee8ae970 100644
--- a/pkg/composer/port.go
+++ b/pkg/composer/port.go
@@ -21,7 +21,7 @@ import (
"fmt"
"io"
- "github.com/containerd/nerdctl/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
)
// PortOptions has args for getting the public port of a given private port/protocol
diff --git a/pkg/composer/projectloader/projectloader.go b/pkg/composer/projectloader/projectloader.go
deleted file mode 100644
index ef979c0d69b..00000000000
--- a/pkg/composer/projectloader/projectloader.go
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package projectloader
-
-import (
- "os"
- "path/filepath"
-
- "github.com/compose-spec/compose-go/loader"
- compose "github.com/compose-spec/compose-go/types"
-)
-
-// Load is used only for unit testing.
-// TODO: Remove
-func Load(fileName, projectName string, envMap map[string]string) (*compose.Project, error) {
- if envMap == nil {
- envMap = make(map[string]string)
- }
- b, err := os.ReadFile(fileName)
- if err != nil {
- return nil, err
- }
-
- wd, err := filepath.Abs(filepath.Dir(fileName))
- if err != nil {
- return nil, err
- }
- var files []compose.ConfigFile
- files = append(files, compose.ConfigFile{Filename: fileName, Content: b})
- return loader.Load(compose.ConfigDetails{
- WorkingDir: wd,
- ConfigFiles: files,
- Environment: envMap,
- }, withProjectName(projectName))
-}
-
-func withProjectName(name string) func(*loader.Options) {
- return func(lOpts *loader.Options) {
- lOpts.SetProjectName(name, true)
- }
-}
diff --git a/pkg/composer/pull.go b/pkg/composer/pull.go
index 59bb50a3cc9..758d342f49e 100644
--- a/pkg/composer/pull.go
+++ b/pkg/composer/pull.go
@@ -21,10 +21,11 @@ import (
"fmt"
"os"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
)
type PullOptions struct {
@@ -32,8 +33,8 @@ type PullOptions struct {
}
func (c *Composer) Pull(ctx context.Context, po PullOptions, services []string) error {
- return c.project.WithServices(services, func(svc types.ServiceConfig) error {
- ps, err := serviceparser.Parse(c.project, svc)
+ return c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error {
+ ps, err := serviceparser.Parse(c.project, *svc)
if err != nil {
return err
}
@@ -42,7 +43,7 @@ func (c *Composer) Pull(ctx context.Context, po PullOptions, services []string)
}
func (c *Composer) pullServiceImage(ctx context.Context, image string, platform string, ps *serviceparser.Service, po PullOptions) error {
- logrus.Infof("Pulling image %s", image)
+ log.G(ctx).Infof("Pulling image %s", image)
var args []string // nolint: prealloc
if platform != "" {
@@ -78,7 +79,7 @@ func (c *Composer) pullServiceImage(ctx context.Context, image string, platform
cmd := c.createNerdctlCmd(ctx, append([]string{"pull"}, args...)...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
diff --git a/pkg/composer/push.go b/pkg/composer/push.go
index ae8c6854e59..5f384601863 100644
--- a/pkg/composer/push.go
+++ b/pkg/composer/push.go
@@ -21,18 +21,19 @@ import (
"fmt"
"os"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
)
type PushOptions struct {
}
func (c *Composer) Push(ctx context.Context, po PushOptions, services []string) error {
- return c.project.WithServices(services, func(svc types.ServiceConfig) error {
- ps, err := serviceparser.Parse(c.project, svc)
+ return c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error {
+ ps, err := serviceparser.Parse(c.project, *svc)
if err != nil {
return err
}
@@ -41,7 +42,7 @@ func (c *Composer) Push(ctx context.Context, po PushOptions, services []string)
}
func (c *Composer) pushServiceImage(ctx context.Context, image string, platform string, ps *serviceparser.Service, po PushOptions) error {
- logrus.Infof("Pushing image %s", image)
+ log.G(ctx).Infof("Pushing image %s", image)
var args []string // nolint: prealloc
if platform != "" {
@@ -61,7 +62,7 @@ func (c *Composer) pushServiceImage(ctx context.Context, image string, platform
cmd := c.createNerdctlCmd(ctx, append([]string{"push"}, args...)...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
diff --git a/pkg/composer/restart.go b/pkg/composer/restart.go
index 1e3ff565b8a..e0ab0285b9a 100644
--- a/pkg/composer/restart.go
+++ b/pkg/composer/restart.go
@@ -21,11 +21,12 @@ import (
"fmt"
"sync"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/labels"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
// RestartOptions stores all option input from `nerdctl compose restart`
@@ -37,16 +38,13 @@ type RestartOptions struct {
// `nerdctl restart CONTAINER_ID` to do the actual job.
func (c *Composer) Restart(ctx context.Context, opt RestartOptions, services []string) error {
// in dependency order
- return c.project.WithServices(services, func(svc types.ServiceConfig) error {
+ return c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error {
containers, err := c.Containers(ctx, svc.Name)
if err != nil {
return err
}
- if err := c.restartContainers(ctx, containers, opt); err != nil {
- return err
- }
- return nil
+ return c.restartContainers(ctx, containers, opt)
})
}
@@ -64,14 +62,14 @@ func (c *Composer) restartContainers(ctx context.Context, containers []container
go func() {
defer rsWG.Done()
info, _ := container.Info(ctx, containerd.WithoutRefreshedMetadata)
- logrus.Infof("Restarting container %s", info.Labels[labels.Name])
+ log.G(ctx).Infof("Restarting container %s", info.Labels[labels.Name])
args := []string{"restart"}
if opt.Timeout != nil {
args = append(args, timeoutArg)
}
args = append(args, container.ID())
if err := c.runNerdctlCmd(ctx, args...); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}()
}
diff --git a/pkg/composer/rm.go b/pkg/composer/rm.go
index 1eec295f466..c58cb024475 100644
--- a/pkg/composer/rm.go
+++ b/pkg/composer/rm.go
@@ -21,13 +21,13 @@ import (
"strings"
"sync"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/strutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// RemoveOptions stores all options when removing compose containers:
@@ -80,14 +80,14 @@ func (c *Composer) removeContainers(ctx context.Context, containers []containerd
if !opt.Stop {
cStatus := formatter.ContainerStatus(ctx, container)
if strings.HasPrefix(cStatus, "Up") {
- logrus.Warnf("Removing container %s failed: container still running.", info.Labels[labels.Name])
+ log.G(ctx).Warnf("Removing container %s failed: container still running.", info.Labels[labels.Name])
return
}
}
- logrus.Infof("Removing container %s", info.Labels[labels.Name])
+ log.G(ctx).Infof("Removing container %s", info.Labels[labels.Name])
if err := c.runNerdctlCmd(ctx, append(args, container.ID())...); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}()
}
@@ -104,9 +104,9 @@ func (c *Composer) removeContainersFromParsedServices(ctx context.Context, conta
rmWG.Add(1)
go func() {
defer rmWG.Done()
- logrus.Infof("Removing container %s", container.Name)
+ log.G(ctx).Infof("Removing container %s", container.Name)
if err := c.runNerdctlCmd(ctx, "rm", "-f", id); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}()
}
diff --git a/pkg/composer/run.go b/pkg/composer/run.go
index 5a6e11f9f5d..0b3c4c72342 100644
--- a/pkg/composer/run.go
+++ b/pkg/composer/run.go
@@ -22,12 +22,14 @@ import (
"fmt"
"sync"
- "github.com/compose-spec/compose-go/loader"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/idgen"
- "github.com/sirupsen/logrus"
+ "github.com/compose-spec/compose-go/v2/format"
+ "github.com/compose-spec/compose-go/v2/types"
"golang.org/x/sync/errgroup"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
)
type RunOptions struct {
@@ -45,6 +47,7 @@ type RunOptions struct {
Detach bool
NoDeps bool
Tty bool
+ SigProxy bool
Interactive bool
Rm bool
User string
@@ -93,8 +96,8 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error {
}
svcs = append(svcs, svc)
} else {
- if err := c.project.WithServices([]string{ro.ServiceName}, func(svc types.ServiceConfig) error {
- svcs = append(svcs, svc)
+ if err := c.project.ForEachService([]string{ro.ServiceName}, func(name string, svc *types.ServiceConfig) error {
+ svcs = append(svcs, *svc)
return nil
}); err != nil {
return err
@@ -126,24 +129,24 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error {
if ro.User != "" {
targetSvc.User = ro.User
}
- if ro.Volume != nil && len(ro.Volume) > 0 {
+ if len(ro.Volume) > 0 {
for _, v := range ro.Volume {
- vc, err := loader.ParseVolume(v)
+ vc, err := format.ParseVolume(v)
if err != nil {
return err
}
targetSvc.Volumes = append(targetSvc.Volumes, vc)
}
}
- if ro.Entrypoint != nil && len(ro.Entrypoint) > 0 {
+ if len(ro.Entrypoint) > 0 {
targetSvc.Entrypoint = make([]string, len(ro.Entrypoint))
copy(targetSvc.Entrypoint, ro.Entrypoint)
}
- if ro.Env != nil && len(ro.Env) > 0 {
+ if len(ro.Env) > 0 {
envs := types.NewMappingWithEquals(ro.Env)
targetSvc.Environment.OverrideBy(envs)
}
- if ro.Label != nil && len(ro.Label) > 0 {
+ if len(ro.Label) > 0 {
label := types.NewMappingWithEquals(ro.Label)
for k, v := range label {
if v != nil {
@@ -160,7 +163,7 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error {
for k := range svcs {
svcs[k].Ports = []types.ServicePortConfig{}
}
- if ro.Publish != nil && len(ro.Publish) > 0 {
+ if len(ro.Publish) > 0 {
for _, p := range ro.Publish {
pc, err := types.ParsePortConfig(p)
if err != nil {
@@ -198,15 +201,11 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error {
return fmt.Errorf("error removing orphaned containers: %s", err)
}
} else {
- logrus.Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
+ log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
}
}
- if err := c.runServices(ctx, parsedServices, ro); err != nil {
- return err
- }
-
- return nil
+ return c.runServices(ctx, parsedServices, ro)
}
func (c *Composer) runServices(ctx context.Context, parsedServices []*serviceparser.Service, ro RunOptions) error {
@@ -216,7 +215,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar
// TODO: parallelize loop for ensuring images (make sure not to mess up tty)
for _, ps := range parsedServices {
- if err := c.ensureServiceImage(ctx, ps, !ro.NoBuild, ro.ForceBuild, BuildOptions{}, ro.QuietPull); err != nil {
+ if err := c.ensureServiceImage(ctx, ps, !ro.NoBuild, ro.ForceBuild, BuildOptions{}, ro.QuietPull, ""); err != nil {
return err
}
}
@@ -234,7 +233,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar
services = append(services, ps.Unparsed.Name)
if len(ps.Containers) != 1 {
- logrus.Warnf("compose run does not support scale but %s is currently %v, automatically it will configure 1", ps.Unparsed.Name, len(ps.Containers))
+ log.G(ctx).Warnf("compose run does not support scale but %s is currently %v, automatically it will configure 1", ps.Unparsed.Name, len(ps.Containers))
}
if len(ps.Containers) == 0 {
@@ -243,7 +242,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar
container := ps.Containers[0]
runEG.Go(func() error {
- id, err := c.upServiceContainer(ctx, ps, container)
+ id, err := c.upServiceContainer(ctx, ps, container, RecreateForce)
if err != nil {
return err
}
@@ -261,14 +260,14 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar
}
if ro.Detach {
- logrus.Printf("%s\n", cid)
+ log.G(ctx).Printf("%s\n", cid)
return nil
}
// TODO: fix it when `nerdctl logs` supports `nerdctl run` without detach
// https://github.com/containerd/nerdctl/blob/v0.22.2/pkg/taskutil/taskutil.go#L55
if !ro.Interactive && !ro.Tty {
- logrus.Info("Attaching to logs")
+ log.G(ctx).Info("Attaching to logs")
lo := LogsOptions{
Follow: true,
NoColor: ro.NoColor,
@@ -280,7 +279,7 @@ func (c *Composer) runServices(ctx context.Context, parsedServices []*servicepar
}
}
- logrus.Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping
+ log.G(ctx).Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping
c.stopContainersFromParsedServices(ctx, containers)
if ro.Rm {
diff --git a/pkg/composer/serviceparser/build.go b/pkg/composer/serviceparser/build.go
index a61e7975af8..c13b264301a 100644
--- a/pkg/composer/serviceparser/build.go
+++ b/pkg/composer/serviceparser/build.go
@@ -22,20 +22,21 @@ import (
"path/filepath"
"strings"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/identifiers"
- "github.com/containerd/nerdctl/pkg/reflectutil"
-
+ "github.com/compose-spec/compose-go/v2/types"
securejoin "github.com/cyphar/filepath-securejoin"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/identifiers"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName string) (*Build, error) {
if unknown := reflectutil.UnknownNonEmptyFields(c,
"Context", "Dockerfile", "Args", "CacheFrom", "Target", "Labels", "Secrets",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: build: %+v", unknown)
+ log.L.Warnf("Ignoring: build: %+v", unknown)
}
if c.Context == "" {
@@ -44,16 +45,13 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st
if strings.Contains(c.Context, "://") {
return nil, fmt.Errorf("build: URL-style context (%q) is not supported yet: %w", c.Context, errdefs.ErrNotImplemented)
}
- if filepath.IsAbs(c.Context) {
- logrus.Warnf("build.config should be relative path, got %q", c.Context)
- }
ctxDir := project.RelativePath(c.Context)
var b Build
b.BuildArgs = append(b.BuildArgs, "-t="+imageName)
if c.Dockerfile != "" {
if filepath.IsAbs(c.Dockerfile) {
- logrus.Warnf("build.dockerfile should be relative path, got %q", c.Dockerfile)
+ log.L.Warnf("build.dockerfile should be relative path, got %q", c.Dockerfile)
b.BuildArgs = append(b.BuildArgs, "-f="+c.Dockerfile)
} else {
// no need to use securejoin
@@ -86,16 +84,18 @@ func parseBuildConfig(c *types.BuildConfig, project *types.Project, imageName st
for _, s := range c.Secrets {
fileRef := types.FileReferenceConfig(s)
- if err := identifiers.Validate(fileRef.Source); err != nil {
- return nil, fmt.Errorf("secret source %q is invalid: %w", fileRef.Source, err)
+
+ if err := identifiers.ValidateDockerCompat(fileRef.Source); err != nil {
+ return nil, fmt.Errorf("invalid secret source name: %w", err)
}
+
projectSecret, ok := project.Secrets[fileRef.Source]
if !ok {
return nil, fmt.Errorf("build: secret %s is undefined", fileRef.Source)
}
var src string
if filepath.IsAbs(projectSecret.File) {
- logrus.Warnf("build.secrets should be relative path, got %q", projectSecret.File)
+ log.L.Warnf("build.secrets should be relative path, got %q", projectSecret.File)
src = projectSecret.File
} else {
var err error
diff --git a/pkg/composer/serviceparser/build_test.go b/pkg/composer/serviceparser/build_test.go
index a789810a0d1..34af7143aec 100644
--- a/pkg/composer/serviceparser/build_test.go
+++ b/pkg/composer/serviceparser/build_test.go
@@ -17,11 +17,12 @@
package serviceparser
import (
+ "runtime"
"testing"
- "github.com/containerd/nerdctl/pkg/composer/projectloader"
- "github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func lastOf(ss []string) string {
@@ -30,6 +31,11 @@ func lastOf(ss []string) string {
func TestParseBuild(t *testing.T) {
t.Parallel()
+
+ if runtime.GOOS == "windows" {
+ t.Skip("test is not compatible with windows")
+ }
+
const dockerComposeYAML = `
services:
foo:
@@ -59,7 +65,7 @@ secrets:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
fooSvc, err := project.GetService("foo")
@@ -85,7 +91,7 @@ secrets:
assert.Equal(t, project.RelativePath("barctx"), lastOf(bar.Build.BuildArgs))
assert.Assert(t, in(bar.Build.BuildArgs, "--target=bartgt"))
assert.Assert(t, in(bar.Build.BuildArgs, "--label=bar=baz"))
- secretPath := project.RelativePath("barctx")
+ secretPath := project.WorkingDir
assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=tgt_secret,src="+secretPath+"/test_secret1"))
assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=simple_secret,src="+secretPath+"/test_secret2"))
assert.Assert(t, in(bar.Build.BuildArgs, "--secret=id=absolute_secret,src=/tmp/absolute_secret"))
diff --git a/pkg/composer/serviceparser/serviceparser.go b/pkg/composer/serviceparser/serviceparser.go
index 785fc97b9f8..2c45a1cecf8 100644
--- a/pkg/composer/serviceparser/serviceparser.go
+++ b/pkg/composer/serviceparser/serviceparser.go
@@ -28,12 +28,13 @@ import (
"strings"
"time"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/containerd/contrib/nvidia"
- "github.com/containerd/containerd/identifiers"
- "github.com/containerd/nerdctl/pkg/reflectutil"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/contrib/nvidia"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/identifiers"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
// ComposeExtensionKey defines fields used to implement extension features.
@@ -55,6 +56,7 @@ const Separator = "-"
func warnUnknownFields(svc types.ServiceConfig) {
if unknown := reflectutil.UnknownNonEmptyFields(&svc,
"Name",
+ "Annotations",
"Build",
"BlkioConfig",
"CapAdd",
@@ -109,14 +111,14 @@ func warnUnknownFields(svc types.ServiceConfig) {
"Volumes",
"Ulimits",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: %+v", svc.Name, unknown)
}
if svc.BlkioConfig != nil {
if unknown := reflectutil.UnknownNonEmptyFields(svc.BlkioConfig,
"Weight",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: blkio_config: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: blkio_config: %+v", svc.Name, unknown)
}
}
@@ -124,13 +126,13 @@ func warnUnknownFields(svc types.ServiceConfig) {
if unknown := reflectutil.UnknownNonEmptyFields(&dep,
"Condition",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: depends_on: %s: %+v", svc.Name, depName, unknown)
+ log.L.Warnf("Ignoring: service %s: depends_on: %s: %+v", svc.Name, depName, unknown)
}
switch dep.Condition {
case "", types.ServiceConditionStarted:
// NOP
default:
- logrus.Warnf("Ignoring: service %s: depends_on: %s: condition %s", svc.Name, depName, dep.Condition)
+ log.L.Warnf("Ignoring: service %s: depends_on: %s: condition %s", svc.Name, depName, dep.Condition)
}
}
@@ -140,34 +142,34 @@ func warnUnknownFields(svc types.ServiceConfig) {
"RestartPolicy",
"Resources",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: deploy: %+v", svc.Name, unknown)
}
if svc.Deploy.RestartPolicy != nil {
if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.RestartPolicy,
"Condition",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy.restart_policy: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: deploy.restart_policy: %+v", svc.Name, unknown)
}
}
if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources,
"Limits",
"Reservations",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy.resources: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: deploy.resources: %+v", svc.Name, unknown)
}
if svc.Deploy.Resources.Limits != nil {
if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources.Limits,
"NanoCPUs",
"MemoryBytes",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy.resources.resources: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: deploy.resources.resources: %+v", svc.Name, unknown)
}
}
if svc.Deploy.Resources.Reservations != nil {
if unknown := reflectutil.UnknownNonEmptyFields(svc.Deploy.Resources.Reservations,
"Devices",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy.resources.resources.reservations: %+v", svc.Name, unknown)
+ log.L.Warnf("Ignoring: service %s: deploy.resources.resources.reservations: %+v", svc.Name, unknown)
}
for i, dev := range svc.Deploy.Resources.Reservations.Devices {
if unknown := reflectutil.UnknownNonEmptyFields(dev,
@@ -176,7 +178,7 @@ func warnUnknownFields(svc types.ServiceConfig) {
"Count",
"IDs",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: service %s: deploy.resources.resources.reservations.devices[%d]: %+v",
+ log.L.Warnf("Ignoring: service %s: deploy.resources.resources.reservations.devices[%d]: %+v",
svc.Name, i, unknown)
}
}
@@ -213,10 +215,10 @@ func getReplicas(svc types.ServiceConfig) (int, error) {
// https://github.com/compose-spec/compose-go/commit/958cb4f953330a3d1303961796d826b7f79132d7
if svc.Deploy != nil && svc.Deploy.Replicas != nil {
- replicas = int(*svc.Deploy.Replicas)
+ replicas = int(*svc.Deploy.Replicas) // nolint:unconvert
}
- if replicas < 1 {
+ if replicas < 0 {
return 0, fmt.Errorf("invalid replicas: %d", replicas)
}
return replicas, nil
@@ -225,15 +227,15 @@ func getReplicas(svc types.ServiceConfig) (int, error) {
func getCPULimit(svc types.ServiceConfig) (string, error) {
var limit string
if svc.CPUS > 0 {
- logrus.Warn("cpus is deprecated, use deploy.resources.limits.cpus")
+ log.L.Warn("cpus is deprecated, use deploy.resources.limits.cpus")
limit = fmt.Sprintf("%f", svc.CPUS)
}
if svc.Deploy != nil && svc.Deploy.Resources.Limits != nil {
- if nanoCPUs := svc.Deploy.Resources.Limits.NanoCPUs; nanoCPUs != "" {
+ if nanoCPUs := svc.Deploy.Resources.Limits.NanoCPUs; nanoCPUs != 0 {
if svc.CPUS > 0 {
- logrus.Warnf("deploy.resources.limits.cpus and cpus (deprecated) must not be set together, ignoring cpus=%f", svc.CPUS)
+ log.L.Warnf("deploy.resources.limits.cpus and cpus (deprecated) must not be set together, ignoring cpus=%f", svc.CPUS)
}
- limit = nanoCPUs
+ limit = strconv.FormatFloat(float64(nanoCPUs), 'f', 2, 32)
}
}
return limit, nil
@@ -242,13 +244,13 @@ func getCPULimit(svc types.ServiceConfig) (string, error) {
func getMemLimit(svc types.ServiceConfig) (types.UnitBytes, error) {
var limit types.UnitBytes
if svc.MemLimit > 0 {
- logrus.Warn("mem_limit is deprecated, use deploy.resources.limits.memory")
+ log.L.Warn("mem_limit is deprecated, use deploy.resources.limits.memory")
limit = svc.MemLimit
}
if svc.Deploy != nil && svc.Deploy.Resources.Limits != nil {
if memoryBytes := svc.Deploy.Resources.Limits.MemoryBytes; memoryBytes > 0 {
if svc.MemLimit > 0 && memoryBytes != svc.MemLimit {
- logrus.Warnf("deploy.resources.limits.memory and mem_limit (deprecated) must not be set together, ignoring mem_limit=%d", svc.MemLimit)
+ log.L.Warnf("deploy.resources.limits.memory and mem_limit (deprecated) must not be set together, ignoring mem_limit=%d", svc.MemLimit)
}
limit = memoryBytes
}
@@ -327,13 +329,13 @@ func getRestart(svc types.ServiceConfig) (string, error) {
if restartFailurePat.MatchString(svc.Restart) {
restartFlag = svc.Restart
} else {
- logrus.Warnf("Ignoring: service %s: restart=%q (unknown)", svc.Name, svc.Restart)
+ log.L.Warnf("Ignoring: service %s: restart=%q (unknown)", svc.Name, svc.Restart)
}
}
if svc.Deploy != nil && svc.Deploy.RestartPolicy != nil {
if svc.Restart != "" {
- logrus.Warnf("deploy.restart_policy and restart must not be set together, ignoring restart=%s", svc.Restart)
+ log.L.Warnf("deploy.restart_policy and restart must not be set together, ignoring restart=%s", svc.Restart)
}
switch cond := svc.Deploy.RestartPolicy.Condition; cond {
case "", "any":
@@ -345,9 +347,9 @@ func getRestart(svc types.ServiceConfig) (string, error) {
case "no":
return "", fmt.Errorf("deploy.restart_policy.condition: \"no\" is invalid, did you mean \"none\"?")
case "on-failure":
- logrus.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unimplemented)", svc.Name, cond)
+ log.L.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unimplemented)", svc.Name, cond)
default:
- logrus.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unknown)", svc.Name, cond)
+ log.L.Warnf("Ignoring: service %s: deploy.restart_policy.condition=%q (unknown)", svc.Name, cond)
}
}
@@ -364,7 +366,7 @@ func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkName
var fullNames []networkNamePair // nolint: prealloc
if svc.Net != "" {
- logrus.Warn("net is deprecated, use network_mode or networks")
+ log.L.Warn("net is deprecated, use network_mode or networks")
if len(svc.Networks) > 0 {
return nil, errors.New("networks and net must not be set together")
}
@@ -383,7 +385,7 @@ func getNetworks(project *types.Project, svc types.ServiceConfig) ([]networkName
return nil, errors.New("net and network_mode must not be set together")
}
if strings.Contains(svc.NetworkMode, ":") {
- if !strings.HasPrefix(svc.NetworkMode, "container:") {
+ if !strings.HasPrefix(svc.NetworkMode, "container:") && !strings.HasPrefix(svc.NetworkMode, "ns:") {
return nil, fmt.Errorf("unsupported network_mode: %q", svc.NetworkMode)
}
}
@@ -448,7 +450,7 @@ func Parse(project *types.Project, svc types.ServiceConfig) (*Service, error) {
parsed.Build.Force = true
parsed.PullMode = "never"
default:
- logrus.Warnf("Ignoring: service %s: pull_policy: %q", svc.Name, svc.PullPolicy)
+ log.L.Warnf("Ignoring: service %s: pull_policy: %q", svc.Name, svc.PullPolicy)
}
for i := 0; i < replicas; i++ {
@@ -478,6 +480,14 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
"--pull=never", // because image will be ensured before running replicas with `nerdctl run`.
}
+ for k, v := range svc.Annotations {
+ if v == "" {
+ c.RunArgs = append(c.RunArgs, fmt.Sprintf("--annotation=%s", k))
+ } else {
+ c.RunArgs = append(c.RunArgs, fmt.Sprintf("--annotation=%s=%s", k, v))
+ }
+ }
+
if svc.BlkioConfig != nil && svc.BlkioConfig.Weight != 0 {
c.RunArgs = append(c.RunArgs, fmt.Sprintf("--blkio-weight=%d", svc.BlkioConfig.Weight))
}
@@ -505,7 +515,7 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
}
for _, v := range svc.Devices {
- c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s", v))
+ c.RunArgs = append(c.RunArgs, fmt.Sprintf("--device=%s:%s:%s", v.Source, v.Target, v.Permissions))
}
for _, v := range svc.DNS {
@@ -530,7 +540,9 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
}
}
for k, v := range svc.ExtraHosts {
- c.RunArgs = append(c.RunArgs, fmt.Sprintf("--add-host=%s:%s", k, v))
+ for _, h := range v {
+ c.RunArgs = append(c.RunArgs, fmt.Sprintf("--add-host=%s:%s", k, h))
+ }
}
if svc.Init != nil && *svc.Init {
@@ -584,6 +596,9 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
if value != nil && value.Ipv4Address != "" {
c.RunArgs = append(c.RunArgs, "--ip="+value.Ipv4Address)
}
+ if value != nil && value.MacAddress != "" {
+ c.RunArgs = append(c.RunArgs, "--mac-address="+value.MacAddress)
+ }
}
}
@@ -674,6 +689,10 @@ func newContainer(project *types.Project, parsed *Service, i int) (*Container, e
c.RunArgs = append(c.RunArgs, "--user="+svc.User)
}
+ for _, v := range svc.GroupAdd {
+ c.RunArgs = append(c.RunArgs, fmt.Sprintf("--group-add=%s", v))
+ }
+
for _, v := range svc.Volumes {
vStr, mkdir, err := serviceVolumeConfigToFlagV(v, project)
if err != nil {
@@ -726,7 +745,7 @@ func servicePortConfigToFlagP(c types.ServicePortConfig) (string, error) {
"Published",
"Protocol",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: port: %+v", unknown)
+ log.L.Warnf("Ignoring: port: %+v", unknown)
}
switch c.Mode {
case "", "ingress":
@@ -759,18 +778,18 @@ func serviceVolumeConfigToFlagV(c types.ServiceVolumeConfig, project *types.Proj
"Bind",
"Volume",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: volume: %+v", unknown)
+ log.L.Warnf("Ignoring: volume: %+v", unknown)
}
if c.Bind != nil {
// c.Bind is expected to be a non-nil reference to an empty Bind struct
if unknown := reflectutil.UnknownNonEmptyFields(c.Bind, "CreateHostPath"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: volume: Bind: %+v", unknown)
+ log.L.Warnf("Ignoring: volume: Bind: %+v", unknown)
}
}
if c.Volume != nil {
// c.Volume is expected to be a non-nil reference to an empty Volume struct
if unknown := reflectutil.UnknownNonEmptyFields(c.Volume); len(unknown) > 0 {
- logrus.Warnf("Ignoring: volume: Volume: %+v", unknown)
+ log.L.Warnf("Ignoring: volume: Volume: %+v", unknown)
}
}
@@ -829,11 +848,11 @@ func fileReferenceConfigToFlagV(c types.FileReferenceConfig, project *types.Proj
if unknown := reflectutil.UnknownNonEmptyFields(&c,
"Source", "Target", "UID", "GID", "Mode",
); len(unknown) > 0 {
- logrus.Warnf("Ignoring: %s: %+v", objType, unknown)
+ log.L.Warnf("Ignoring: %s: %+v", objType, unknown)
}
- if err := identifiers.Validate(c.Source); err != nil {
- return "", fmt.Errorf("%s source %q is invalid: %w", objType, c.Source, err)
+ if err := identifiers.ValidateDockerCompat(c.Source); err != nil {
+ return "", fmt.Errorf("invalid source name for %s: %w", objType, err)
}
var obj types.FileObjectConfig
diff --git a/pkg/composer/serviceparser/serviceparser_test.go b/pkg/composer/serviceparser/serviceparser_test.go
index 308e7cb76e4..c3eaee0cd49 100644
--- a/pkg/composer/serviceparser/serviceparser_test.go
+++ b/pkg/composer/serviceparser/serviceparser_test.go
@@ -20,14 +20,15 @@ import (
"fmt"
"os"
"path/filepath"
+ "runtime"
"strconv"
"testing"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/projectloader"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/containerd/nerdctl/pkg/testutil"
+ "github.com/compose-spec/compose-go/v2/types"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/testutil"
)
func TestServicePortConfigToFlagP(t *testing.T) {
@@ -79,6 +80,11 @@ var in = strutil.InStringSlice
func TestParse(t *testing.T) {
t.Parallel()
+
+ if runtime.GOOS == "windows" {
+ t.Skip("test is not compatible with windows")
+ }
+
const dockerComposeYAML = `
version: '3.1'
@@ -117,6 +123,9 @@ services:
options:
max-size: "5K"
max-file: "2"
+ user: 1001:1001
+ group_add:
+ - "1001"
db:
image: mariadb:10.5
@@ -138,7 +147,7 @@ volumes:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
wpSvc, err := project.GetService("wordpress")
@@ -174,6 +183,8 @@ volumes:
assert.Assert(t, in(wp1.RunArgs, "--add-host=test.com:172.19.1.1"))
assert.Assert(t, in(wp1.RunArgs, "--add-host=test2.com:172.19.1.2"))
assert.Assert(t, in(wp1.RunArgs, "--shm-size=1073741824"))
+ assert.Assert(t, in(wp1.RunArgs, "--user=1001:1001"))
+ assert.Assert(t, in(wp1.RunArgs, "--group-add=1001"))
dbSvc, err := project.GetService("db")
assert.NilError(t, err)
@@ -197,7 +208,7 @@ func TestParseDeprecated(t *testing.T) {
services:
foo:
image: nginx:alpine
- # scale is deprecated in favor of deploy.replicas, but still valid
+ # scale was deprecated in favor of deploy.replicas, and is now ignored
scale: 2
# cpus is deprecated in favor of deploy.resources.limits.cpu, but still valid
cpus: 0.42
@@ -207,7 +218,7 @@ services:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
fooSvc, err := project.GetService("foo")
@@ -217,7 +228,7 @@ services:
assert.NilError(t, err)
t.Logf("foo: %+v", foo)
- assert.Assert(t, len(foo.Containers) == 2)
+ assert.Assert(t, len(foo.Containers) == 1)
for i, c := range foo.Containers {
assert.Assert(t, c.Name == DefaultContainerName(project.Name, "foo", strconv.Itoa(i+1)))
assert.Assert(t, in(c.RunArgs, "--name="+c.Name))
@@ -260,11 +271,15 @@ services:
devices:
- capabilities: ["utility"]
count: all
+ qux: # replicas=0
+ image: nginx:alpine
+ deploy:
+ replicas: 0
`
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
fooSvc, err := project.GetService("foo")
@@ -310,10 +325,54 @@ services:
assert.Assert(t, in(c.RunArgs, "--restart=no"))
assert.Assert(t, in(c.RunArgs, `--gpus=capabilities=utility,count=-1`))
}
+
+ quxSvc, err := project.GetService("qux")
+ assert.NilError(t, err)
+
+ qux, err := Parse(project, quxSvc)
+ assert.NilError(t, err)
+
+ t.Logf("qux: %+v", qux)
+ assert.Assert(t, len(qux.Containers) == 0)
+
+}
+
+func TestParseDevices(t *testing.T) {
+ const dockerComposeYAML = `
+services:
+ foo:
+ image: nginx:alpine
+ devices:
+ - /dev/a
+ - /dev/b:/dev/b
+ - /dev/c:/dev/c:rw
+`
+ comp := testutil.NewComposeDir(t, dockerComposeYAML)
+ defer comp.CleanUp()
+
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ assert.NilError(t, err)
+
+ fooSvc, err := project.GetService("foo")
+ assert.NilError(t, err)
+
+ foo, err := Parse(project, fooSvc)
+ assert.NilError(t, err)
+
+ t.Logf("foo: %+v", foo)
+ for _, c := range foo.Containers {
+ assert.Assert(t, in(c.RunArgs, "--device=/dev/a:/dev/a:rwm"))
+ assert.Assert(t, in(c.RunArgs, "--device=/dev/b:/dev/b:rwm"))
+ assert.Assert(t, in(c.RunArgs, "--device=/dev/c:/dev/c:rw"))
+ }
}
func TestParseRelative(t *testing.T) {
t.Parallel()
+
+ if runtime.GOOS == "windows" {
+ t.Skip("test is not compatible with windows")
+ }
const dockerComposeYAML = `
services:
foo:
@@ -327,7 +386,7 @@ services:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
fooSvc, err := project.GetService("foo")
@@ -359,7 +418,7 @@ services:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
fooSvc, err := project.GetService("foo")
@@ -389,6 +448,9 @@ services:
func TestParseConfigs(t *testing.T) {
t.Parallel()
+ if runtime.GOOS == "windows" {
+ t.Skip("test is not compatible with windows")
+ }
const dockerComposeYAML = `
services:
foo:
@@ -419,7 +481,7 @@ configs:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
for _, f := range []string{"secret1", "secret2", "secret3", "config1", "config2"} {
@@ -463,7 +525,7 @@ services:
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
- project, err := projectloader.Load(comp.YAMLFullPath(), comp.ProjectName(), nil)
+ project, err := testutil.LoadProject(comp.YAMLFullPath(), comp.ProjectName(), nil)
assert.NilError(t, err)
getContainersFromService := func(svcName string) []Container {
diff --git a/pkg/composer/stop.go b/pkg/composer/stop.go
index 863563b1d23..7f514cedaff 100644
--- a/pkg/composer/stop.go
+++ b/pkg/composer/stop.go
@@ -21,12 +21,12 @@ import (
"fmt"
"sync"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/strutil"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/log"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// StopOptions stores all option input from `nerdctl compose stop`
@@ -68,14 +68,14 @@ func (c *Composer) stopContainers(ctx context.Context, containers []containerd.C
go func() {
defer rmWG.Done()
info, _ := container.Info(ctx, containerd.WithoutRefreshedMetadata)
- logrus.Infof("Stopping container %s", info.Labels[labels.Name])
+ log.G(ctx).Infof("Stopping container %s", info.Labels[labels.Name])
args := []string{"stop"}
if opt.Timeout != nil {
args = append(args, timeoutArg)
}
args = append(args, container.ID())
if err := c.runNerdctlCmd(ctx, args...); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}()
}
@@ -92,9 +92,9 @@ func (c *Composer) stopContainersFromParsedServices(ctx context.Context, contain
rmWG.Add(1)
go func() {
defer rmWG.Done()
- logrus.Infof("Stopping container %s", container.Name)
+ log.G(ctx).Infof("Stopping container %s", container.Name)
if err := c.runNerdctlCmd(ctx, "stop", id); err != nil {
- logrus.Warn(err)
+ log.G(ctx).Warn(err)
}
}()
}
diff --git a/pkg/composer/up.go b/pkg/composer/up.go
index 40eb752d0a1..98106c81ae4 100644
--- a/pkg/composer/up.go
+++ b/pkg/composer/up.go
@@ -21,23 +21,39 @@ import (
"fmt"
"os"
- "github.com/compose-spec/compose-go/types"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/reflectutil"
+ "github.com/compose-spec/compose-go/v2/types"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
type UpOptions struct {
- Detach bool
- NoBuild bool
- NoColor bool
- NoLogPrefix bool
- ForceBuild bool
- IPFS bool
- QuietPull bool
- RemoveOrphans bool
- Scale map[string]uint64 // map of service name to replicas
+ AbortOnContainerExit bool
+ Detach bool
+ NoBuild bool
+ NoColor bool
+ NoLogPrefix bool
+ ForceBuild bool
+ IPFS bool
+ QuietPull bool
+ RemoveOrphans bool
+ ForceRecreate bool
+ NoRecreate bool
+ Scale map[string]int // map of service name to replicas
+ Pull string
+}
+
+func (opts UpOptions) recreateStrategy() string {
+ switch {
+ case opts.ForceRecreate:
+ return RecreateForce
+ case opts.NoRecreate:
+ return RecreateNever
+ default:
+ return RecreateDiverged
+ }
}
func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) error {
@@ -69,14 +85,14 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro
var parsedServices []*serviceparser.Service
// use WithServices to sort the services in dependency order
- if err := c.project.WithServices(services, func(svc types.ServiceConfig) error {
+ if err := c.project.ForEachService(services, func(name string, svc *types.ServiceConfig) error {
if replicas, ok := uo.Scale[svc.Name]; ok {
if svc.Deploy == nil {
svc.Deploy = &types.DeployConfig{}
}
svc.Deploy.Replicas = &replicas
}
- ps, err := serviceparser.Parse(c.project, svc)
+ ps, err := serviceparser.Parse(c.project, *svc)
if err != nil {
return err
}
@@ -98,24 +114,18 @@ func (c *Composer) Up(ctx context.Context, uo UpOptions, services []string) erro
return fmt.Errorf("error removing orphaned containers: %s", err)
}
} else {
- logrus.Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
+ log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans)
}
}
- if err := c.upServices(ctx, parsedServices, uo); err != nil {
- return err
- }
-
- return nil
+ return c.upServices(ctx, parsedServices, uo)
}
func validateFileObjectConfig(obj types.FileObjectConfig, shortName, objType string, project *types.Project) error {
if unknown := reflectutil.UnknownNonEmptyFields(&obj, "Name", "External", "File"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: %s %s: %+v", objType, shortName, unknown)
- }
- if obj.External.External || obj.External.Name != "" {
- return fmt.Errorf("%s %q: external object is not supported", objType, shortName)
+ log.L.Warnf("Ignoring: %s %s: %+v", objType, shortName, unknown)
}
+
if obj.File == "" {
return fmt.Errorf("%s %q: lacks file path", objType, shortName)
}
diff --git a/pkg/composer/up_network.go b/pkg/composer/up_network.go
index c9c76e6b7c4..a68b63c0f71 100644
--- a/pkg/composer/up_network.go
+++ b/pkg/composer/up_network.go
@@ -20,10 +20,10 @@ import (
"context"
"fmt"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/reflectutil"
+ "github.com/containerd/log"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
func (c *Composer) upNetwork(ctx context.Context, shortName string) error {
@@ -31,13 +31,13 @@ func (c *Composer) upNetwork(ctx context.Context, shortName string) error {
if !ok {
return fmt.Errorf("invalid network name %q", shortName)
}
- if net.External.External {
+ if net.External {
// NOP
return nil
}
if unknown := reflectutil.UnknownNonEmptyFields(&net, "Name", "Ipam", "Driver", "DriverOpts"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: network %s: %+v", shortName, unknown)
+ log.G(ctx).Warnf("Ignoring: network %s: %+v", shortName, unknown)
}
// shortName is like "default", fullName is like "compose-wordpress_default"
@@ -46,7 +46,7 @@ func (c *Composer) upNetwork(ctx context.Context, shortName string) error {
if err != nil {
return err
} else if !netExists {
- logrus.Infof("Creating network %s", fullName)
+ log.G(ctx).Infof("Creating network %s", fullName)
//add metadata labels to network https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels-1
createArgs := []string{
fmt.Sprintf("--label=%s=%s", labels.ComposeProject, c.project.Name),
@@ -65,12 +65,12 @@ func (c *Composer) upNetwork(ctx context.Context, shortName string) error {
if net.Ipam.Config != nil {
if len(net.Ipam.Config) > 1 {
- logrus.Warnf("Ignoring: network %s: imam.config %+v", shortName, net.Ipam.Config[1:])
+ log.G(ctx).Warnf("Ignoring: network %s: imam.config %+v", shortName, net.Ipam.Config[1:])
}
ipamConfig := net.Ipam.Config[0]
if unknown := reflectutil.UnknownNonEmptyFields(ipamConfig, "Subnet", "Gateway", "IPRange"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: network %s: ipam.config[0]: %+v", shortName, unknown)
+ log.G(ctx).Warnf("Ignoring: network %s: ipam.config[0]: %+v", shortName, unknown)
}
if ipamConfig.Subnet != "" {
createArgs = append(createArgs, fmt.Sprintf("--subnet=%s", ipamConfig.Subnet))
@@ -86,7 +86,7 @@ func (c *Composer) upNetwork(ctx context.Context, shortName string) error {
createArgs = append(createArgs, fullName)
if c.DebugPrintFull {
- logrus.Debugf("Creating network args: %s", createArgs)
+ log.G(ctx).Debugf("Creating network args: %s", createArgs)
}
if err := c.runNerdctlCmd(ctx, append([]string{"network", "create"}, createArgs...)...); err != nil {
diff --git a/pkg/composer/up_service.go b/pkg/composer/up_service.go
index 5aa5d3417f0..f0da7c9b72a 100644
--- a/pkg/composer/up_service.go
+++ b/pkg/composer/up_service.go
@@ -21,15 +21,17 @@ import (
"errors"
"fmt"
"os"
+ "os/exec"
"path/filepath"
"strings"
"sync"
- "github.com/containerd/nerdctl/pkg/composer/serviceparser"
- "github.com/containerd/nerdctl/pkg/labels"
-
- "github.com/sirupsen/logrus"
"golang.org/x/sync/errgroup"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/composer/serviceparser"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
func (c *Composer) upServices(ctx context.Context, parsedServices []*serviceparser.Service, uo UpOptions) error {
@@ -39,11 +41,13 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars
// TODO: parallelize loop for ensuring images (make sure not to mess up tty)
for _, ps := range parsedServices {
- if err := c.ensureServiceImage(ctx, ps, !uo.NoBuild, uo.ForceBuild, BuildOptions{}, uo.QuietPull); err != nil {
+ if err := c.ensureServiceImage(ctx, ps, !uo.NoBuild, uo.ForceBuild, BuildOptions{}, uo.QuietPull, uo.Pull); err != nil {
return err
}
}
+ recreate := uo.recreateStrategy()
+
var (
containers = make(map[string]serviceparser.Container) // key: container ID
services = []string{}
@@ -56,7 +60,7 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars
for _, container := range ps.Containers {
container := container
runEG.Go(func() error {
- id, err := c.upServiceContainer(ctx, ps, container)
+ id, err := c.upServiceContainer(ctx, ps, container, recreate)
if err != nil {
return err
}
@@ -75,22 +79,29 @@ func (c *Composer) upServices(ctx context.Context, parsedServices []*servicepars
return nil
}
- logrus.Info("Attaching to logs")
+ // this is used to stop containers in case --abort-on-container-exit flag is set.
+ // c.Logs returns an error, so we don't need Ctrl-c to reach the "Stopping containers (forcibly)"
+ if uo.AbortOnContainerExit {
+ defer c.stopContainersFromParsedServices(ctx, containers)
+ }
+ log.G(ctx).Info("Attaching to logs")
lo := LogsOptions{
- Follow: true,
- NoColor: uo.NoColor,
- NoLogPrefix: uo.NoLogPrefix,
+ AbortOnContainerExit: uo.AbortOnContainerExit,
+ Follow: true,
+ NoColor: uo.NoColor,
+ NoLogPrefix: uo.NoLogPrefix,
+ LatestRun: recreate == RecreateNever,
}
if err := c.Logs(ctx, lo, services); err != nil {
return err
}
- logrus.Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping
+ log.G(ctx).Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping
c.stopContainersFromParsedServices(ctx, containers)
return nil
}
-func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool) error {
+func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool, pullModeArg string) error {
if ps.Build != nil && allowBuild {
if ps.Build.Force || forceBuild {
return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo)
@@ -102,39 +113,59 @@ func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Ser
}
// even when c.ImageExists returns true, we need to call c.EnsureImage
// because ps.PullMode can be "always". So no return here.
- logrus.Debugf("Image %s already exists, not building", ps.Image)
+ log.G(ctx).Debugf("Image %s already exists, not building", ps.Image)
}
- logrus.Infof("Ensuring image %s", ps.Image)
- if err := c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet); err != nil {
- return err
+ log.G(ctx).Infof("Ensuring image %s", ps.Image)
+ if pullModeArg != "" {
+ return c.EnsureImage(ctx, ps.Image, pullModeArg, ps.Unparsed.Platform, ps, quiet)
}
- return nil
+ return c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet)
}
// upServiceContainer must be called after ensureServiceImage
// upServiceContainer returns container ID
-func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container) (string, error) {
+func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container, recreate string) (string, error) {
// check if container already exists
- exists, err := c.containerExists(ctx, container.Name, service.Unparsed.Name)
+ existingCid, err := c.containerID(ctx, container.Name, service.Unparsed.Name)
if err != nil {
return "", fmt.Errorf("error while checking for containers with name %q: %s", container.Name, err)
}
+ // FIXME
+ if service.Unparsed.StdinOpen != service.Unparsed.Tty {
+ return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same")
+ }
+
+ var runFlagD bool
+ if !service.Unparsed.StdinOpen && !service.Unparsed.Tty {
+ container.RunArgs = append([]string{"-d"}, container.RunArgs...)
+ runFlagD = true
+ }
+
+ // start the existing container and exit early
+ if existingCid != "" && recreate == RecreateNever {
+ cmd := c.createNerdctlCmd(ctx, append([]string{"start"}, existingCid)...)
+ if err := c.executeUpCmd(ctx, cmd, container.Name, runFlagD, service.Unparsed.StdinOpen); err != nil {
+ return "", fmt.Errorf("error while starting existing container %s: %w", container.Name, err)
+ }
+ return existingCid, nil
+ }
+
// delete container if it already exists
- if exists {
- logrus.Debugf("Container %q already exists, deleting", container.Name)
+ if existingCid != "" {
+ log.G(ctx).Debugf("Container %q already exists, deleting", container.Name)
delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name)
if err = delCmd.Run(); err != nil {
return "", fmt.Errorf("could not delete container %q: %s", container.Name, err)
}
- logrus.Infof("Re-creating container %s", container.Name)
+ log.G(ctx).Infof("Re-creating container %s", container.Name)
} else {
- logrus.Infof("Creating container %s", container.Name)
+ log.G(ctx).Infof("Creating container %s", container.Name)
}
for _, f := range container.Mkdir {
- logrus.Debugf("Creating a directory %q", f)
+ log.G(ctx).Debugf("Creating a directory %q", f)
if err = os.MkdirAll(f, 0o755); err != nil {
return "", fmt.Errorf("failed to create a directory %q: %w", f, err)
}
@@ -147,10 +178,8 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse
defer os.RemoveAll(tempDir)
cidFilename := filepath.Join(tempDir, "cid")
- var runFlagD bool
- if !service.Unparsed.StdinOpen && !service.Unparsed.Tty {
- container.RunArgs = append([]string{"-d"}, container.RunArgs...)
- runFlagD = true
+ if c.EnvFile != "" {
+ container.RunArgs = append([]string{"--env-file=" + c.EnvFile}, container.RunArgs...)
}
//add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels
@@ -162,15 +191,27 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse
cmd := c.createNerdctlCmd(ctx, append([]string{"run"}, container.RunArgs...)...)
if c.DebugPrintFull {
- logrus.Debugf("Running %v", cmd.Args)
+ log.G(ctx).Debugf("Running %v", cmd.Args)
}
- // FIXME
- if service.Unparsed.StdinOpen != service.Unparsed.Tty {
- return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same")
+ if err := c.executeUpCmd(ctx, cmd, container.Name, runFlagD, service.Unparsed.StdinOpen); err != nil {
+ return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
+ }
+
+ cid, err := os.ReadFile(cidFilename)
+ if err != nil {
+ return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
}
+ return strings.TrimSpace(string(cid)), nil
+}
- if service.Unparsed.StdinOpen {
+func (c *Composer) executeUpCmd(ctx context.Context, cmd *exec.Cmd, containerName string, runFlagD, stdinOpen bool) error {
+ log.G(ctx).Infof("Running %v", cmd.Args)
+ if c.DebugPrintFull {
+ log.G(ctx).Debugf("Running %v", cmd.Args)
+ }
+
+ if stdinOpen {
cmd.Stdin = os.Stdin
}
if !runFlagD {
@@ -179,14 +220,9 @@ func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparse
// Always propagate stderr to print detailed error messages (https://github.com/containerd/nerdctl/issues/1942)
cmd.Stderr = os.Stderr
- err = cmd.Run()
- if err != nil {
- return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("error while creating container %s: %w", containerName, err)
}
- cid, err := os.ReadFile(cidFilename)
- if err != nil {
- return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
- }
- return strings.TrimSpace(string(cid)), nil
+ return nil
}
diff --git a/pkg/composer/up_volume.go b/pkg/composer/up_volume.go
index 3e8e26c5012..0ab0db1fd7a 100644
--- a/pkg/composer/up_volume.go
+++ b/pkg/composer/up_volume.go
@@ -20,10 +20,10 @@ import (
"context"
"fmt"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/reflectutil"
+ "github.com/containerd/log"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/reflectutil"
)
func (c *Composer) upVolume(ctx context.Context, shortName string) error {
@@ -31,22 +31,25 @@ func (c *Composer) upVolume(ctx context.Context, shortName string) error {
if !ok {
return fmt.Errorf("invalid volume name %q", shortName)
}
- if vol.External.External {
+ if vol.External {
// NOP
return nil
}
if unknown := reflectutil.UnknownNonEmptyFields(&vol, "Name"); len(unknown) > 0 {
- logrus.Warnf("Ignoring: volume %s: %+v", shortName, unknown)
+ log.G(ctx).Warnf("Ignoring: volume %s: %+v", shortName, unknown)
}
// shortName is like "db_data", fullName is like "compose-wordpress_db_data"
fullName := vol.Name
+ // FIXME: this is racy. By the time we get below to creating the volume, there is no guarantee that things are still fine.
+ // Furthermore, by the time we are done creating all the volumes, they may very well have been destroyed.
+ // This cannot be fixed without getting rid of the whole "shell-out" approach entirely.
volExists, err := c.VolumeExists(fullName)
if err != nil {
return err
} else if !volExists {
- logrus.Infof("Creating volume %s", fullName)
+ log.G(ctx).Infof("Creating volume %s", fullName)
//add metadata labels to volume https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels-2
createArgs := []string{
fmt.Sprintf("--label=%s=%s", labels.ComposeProject, c.project.Name),
diff --git a/pkg/config/config.go b/pkg/config/config.go
index e393933779d..1666ab61a0e 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -17,10 +17,10 @@
package config
import (
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/defaults"
- "github.com/containerd/containerd/namespaces"
- ncdefaults "github.com/containerd/nerdctl/pkg/defaults"
+ "github.com/containerd/containerd/v2/defaults"
+ "github.com/containerd/containerd/v2/pkg/namespaces"
+
+ ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults"
)
// Config corresponds to nerdctl.toml .
@@ -39,6 +39,8 @@ type Config struct {
HostsDir []string `toml:"hosts_dir"`
Experimental bool `toml:"experimental"`
HostGatewayIP string `toml:"host_gateway_ip"`
+ BridgeIP string `toml:"bridge_ip, omitempty"`
+ KubeHideDupe bool `toml:"kube_hide_dupe"`
}
// New creates a default Config object statically,
@@ -49,7 +51,7 @@ func New() *Config {
DebugFull: false,
Address: defaults.DefaultAddress,
Namespace: namespaces.Default,
- Snapshotter: containerd.DefaultSnapshotter,
+ Snapshotter: defaults.DefaultSnapshotter,
CNIPath: ncdefaults.CNIPath(),
CNINetConfPath: ncdefaults.CNINetConfPath(),
DataRoot: ncdefaults.DataRoot(),
@@ -58,5 +60,6 @@ func New() *Config {
HostsDir: ncdefaults.HostsDirs(),
Experimental: true,
HostGatewayIP: ncdefaults.HostGatewayIP(),
+ KubeHideDupe: false,
}
}
diff --git a/pkg/consoleutil/consoleutil.go b/pkg/consoleutil/consoleutil.go
index 4f9167a0b4c..a19988a6895 100644
--- a/pkg/consoleutil/consoleutil.go
+++ b/pkg/consoleutil/consoleutil.go
@@ -18,8 +18,22 @@ package consoleutil
import (
"context"
+ "os"
+
+ "github.com/containerd/console"
)
+// Current is from https://github.com/containerd/console/blob/v1.0.4/console.go#L68-L81
+// adapted so that it does not panic
+func Current() (c console.Console, err error) {
+ for _, s := range []*os.File{os.Stderr, os.Stdout, os.Stdin} {
+ if c, err = console.ConsoleFromFile(s); err == nil {
+ return c, nil
+ }
+ }
+ return nil, console.ErrNotAConsole
+}
+
// resizer is from https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/tasks/tasks.go#L25-L27
type resizer interface {
Resize(ctx context.Context, w, h uint32) error
diff --git a/pkg/consoleutil/consoleutil_unix.go b/pkg/consoleutil/consoleutil_unix.go
index 17421a6fed4..5cc05685021 100644
--- a/pkg/consoleutil/consoleutil_unix.go
+++ b/pkg/consoleutil/consoleutil_unix.go
@@ -1,4 +1,4 @@
-//go:build !windows
+//go:build unix
/*
Copyright The containerd Authors.
@@ -19,18 +19,19 @@
package consoleutil
import (
- gocontext "context"
+ "context"
"os"
"os/signal"
- "github.com/containerd/console"
- "github.com/containerd/containerd/log"
"golang.org/x/sys/unix"
+
+ "github.com/containerd/console"
+ "github.com/containerd/log"
)
// HandleConsoleResize resizes the console.
// From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/tasks/tasks_unix.go#L43-L68
-func HandleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error {
+func HandleConsoleResize(ctx context.Context, task resizer, con console.Console) error {
// do an initial resize of the console
size, err := con.Size()
if err != nil {
diff --git a/pkg/consoleutil/consoleutil_windows.go b/pkg/consoleutil/consoleutil_windows.go
index ee823786f2a..4ae19c3cc57 100644
--- a/pkg/consoleutil/consoleutil_windows.go
+++ b/pkg/consoleutil/consoleutil_windows.go
@@ -17,16 +17,16 @@
package consoleutil
import (
- gocontext "context"
+ "context"
"time"
"github.com/containerd/console"
- "github.com/containerd/containerd/log"
+ "github.com/containerd/log"
)
// HandleConsoleResize resizes the console.
// From https://github.com/containerd/containerd/blob/v1.7.0-rc.2/cmd/ctr/commands/tasks/tasks_windows.go#L34-L61
-func HandleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error {
+func HandleConsoleResize(ctx context.Context, task resizer, con console.Console) error {
// do an initial resize of the console
size, err := con.Size()
if err != nil {
diff --git a/pkg/consoleutil/detach.go b/pkg/consoleutil/detach.go
index 94b550cb143..daee28ee8f9 100644
--- a/pkg/consoleutil/detach.go
+++ b/pkg/consoleutil/detach.go
@@ -22,7 +22,8 @@ import (
"io"
"github.com/moby/term"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/log"
)
const DefaultDetachKeys = "ctrl-p,ctrl-q"
@@ -53,7 +54,7 @@ func (ds *detachableStdin) Read(p []byte) (int, error) {
n, err := ds.stdin.Read(p)
var eerr term.EscapeError
if errors.As(err, &eerr) {
- logrus.Info("read detach keys")
+ log.L.Info("read detach keys")
if ds.closer != nil {
ds.closer()
}
diff --git a/pkg/containerdutil/content.go b/pkg/containerdutil/content.go
new file mode 100644
index 00000000000..929e60951c9
--- /dev/null
+++ b/pkg/containerdutil/content.go
@@ -0,0 +1,90 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package containerdutil provides "caching" versions of containerd native snapshotter and content store.
+// NOTE: caching should only be used for single, atomic operations, like `nerdctl images`, and NOT kept
+// across successive, distincts operations. As such, caching is not persistent across invocations of nerdctl,
+// and only lasts as long as the lifetime of the Snapshotter or ContentStore.
+package containerdutil
+
+import (
+ "context"
+
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+)
+
+// ContentStore should be called to get a Provider with caching
+func NewProvider(client *containerd.Client) content.Provider {
+ return &providerWithCache{
+ client.ContentStore(),
+ make(map[string]*readerAtWithCache),
+ }
+}
+
+type providerWithCache struct {
+ native content.Provider
+ cache map[string]*readerAtWithCache
+}
+
+func (provider *providerWithCache) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.ReaderAt, error) {
+ key := desc.Digest.String()
+ // If we had en entry already, get the size over
+ value, ok := provider.cache[key]
+ if !ok {
+ newReaderAt, err := provider.native.ReaderAt(ctx, desc)
+ if err != nil {
+ return nil, err
+ }
+ // Build the final object
+ value = &readerAtWithCache{
+ newReaderAt,
+ -1,
+ func() {
+ delete(provider.cache, key)
+ },
+ }
+ // Cache it
+ provider.cache[key] = value
+ }
+
+ return value, nil
+}
+
+// ReaderAtWithCache implements the content.ReaderAt interface
+type readerAtWithCache struct {
+ content.ReaderAt
+ size int64
+ prune func()
+}
+
+func (rac *readerAtWithCache) Size() int64 {
+ // local implementation in containerd technically provides a similar mechanism, so, this method not really useful
+ // by default - but obviously, this is implementation dependent
+ if rac.size == -1 {
+ rac.size = rac.ReaderAt.Size()
+ }
+ return rac.size
+}
+
+func (rac *readerAtWithCache) Close() error {
+ err := rac.ReaderAt.Close()
+ // Remove ourselves from the cache
+ rac.prune()
+ return err
+}
diff --git a/pkg/containerdutil/helpers.go b/pkg/containerdutil/helpers.go
new file mode 100644
index 00000000000..6f4e7a8a451
--- /dev/null
+++ b/pkg/containerdutil/helpers.go
@@ -0,0 +1,46 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package containerdutil
+
+import (
+ "context"
+
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "github.com/containerd/containerd/v2/core/content"
+)
+
+var ReadBlob = readBlobWithCache()
+
+type readBlob func(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]byte, error)
+
+func readBlobWithCache() readBlob {
+ var cache = make(map[string]([]byte))
+
+ return func(ctx context.Context, provider content.Provider, desc ocispec.Descriptor) ([]byte, error) {
+ var err error
+ v, ok := cache[desc.Digest.String()]
+ if !ok {
+ v, err = content.ReadBlob(ctx, provider, desc)
+ if err == nil {
+ cache[desc.Digest.String()] = v
+ }
+ }
+
+ return v, err
+ }
+}
diff --git a/pkg/containerdutil/snapshotter.go b/pkg/containerdutil/snapshotter.go
new file mode 100644
index 00000000000..bf35171b5cb
--- /dev/null
+++ b/pkg/containerdutil/snapshotter.go
@@ -0,0 +1,61 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package containerdutil
+
+import (
+ "context"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/snapshots"
+)
+
+// SnapshotService should be called to get a new caching snapshotter
+func SnapshotService(client *containerd.Client, snapshotterName string) snapshots.Snapshotter {
+ return &snapshotterWithCache{
+ client.SnapshotService(snapshotterName),
+ map[string]snapshots.Info{},
+ map[string]snapshots.Usage{},
+ }
+}
+
+type snapshotterWithCache struct {
+ snapshots.Snapshotter
+ statCache map[string]snapshots.Info
+ usageCache map[string]snapshots.Usage
+}
+
+func (snap *snapshotterWithCache) Stat(ctx context.Context, key string) (snapshots.Info, error) {
+ if stat, ok := snap.statCache[key]; ok {
+ return stat, nil
+ }
+ stat, err := snap.Snapshotter.Stat(ctx, key)
+ if err == nil {
+ snap.statCache[key] = stat
+ }
+ return stat, err
+}
+
+func (snap *snapshotterWithCache) Usage(ctx context.Context, key string) (snapshots.Usage, error) {
+ if usage, ok := snap.usageCache[key]; ok {
+ return usage, nil
+ }
+ usage, err := snap.Snapshotter.Usage(ctx, key)
+ if err == nil {
+ snap.usageCache[key] = usage
+ }
+ return usage, err
+}
diff --git a/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go
index 8f00eb5bf85..a3a77e1e60d 100644
--- a/pkg/containerinspector/containerinspector.go
+++ b/pkg/containerinspector/containerinspector.go
@@ -19,10 +19,12 @@ package containerinspector
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
"github.com/containerd/typeurl/v2"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
func Inspect(ctx context.Context, container containerd.Container) (*native.Container, error) {
@@ -37,12 +39,14 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta
n.Spec, err = typeurl.UnmarshalAny(info.Spec)
if err != nil {
- logrus.WithError(err).WithField("id", id).Warnf("failed to inspect Spec")
+ log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect Spec")
return n, nil
}
task, err := container.Task(ctx, nil)
if err != nil {
- logrus.WithError(err).WithField("id", id).Warnf("failed to inspect Task")
+ if !errdefs.IsNotFound(err) {
+ log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect Task")
+ }
return n, nil
}
n.Process = &native.Process{
@@ -50,13 +54,13 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta
}
st, err := task.Status(ctx)
if err != nil {
- logrus.WithError(err).WithField("id", id).Warnf("failed to inspect Status")
+ log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect Status")
return n, nil
}
n.Process.Status = st
netNS, err := InspectNetNS(ctx, n.Process.Pid)
if err != nil {
- logrus.WithError(err).WithField("id", id).Warnf("failed to inspect NetNS")
+ log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect NetNS")
return n, nil
}
n.Process.NetNS = netNS
diff --git a/pkg/containerinspector/containerinspector_freebsd.go b/pkg/containerinspector/containerinspector_freebsd.go
index c9c53558955..e5a2dbc42fb 100644
--- a/pkg/containerinspector/containerinspector_freebsd.go
+++ b/pkg/containerinspector/containerinspector_freebsd.go
@@ -19,7 +19,7 @@ package containerinspector
import (
"context"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
func InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error) {
diff --git a/pkg/containerinspector/containerinspector_linux.go b/pkg/containerinspector/containerinspector_linux.go
index b409166d85e..729f878b764 100644
--- a/pkg/containerinspector/containerinspector_linux.go
+++ b/pkg/containerinspector/containerinspector_linux.go
@@ -22,9 +22,9 @@ import (
"net"
"strings"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
-
"github.com/containernetworking/plugins/pkg/ns"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
func InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error) {
diff --git a/pkg/containerinspector/containerinspector_windows.go b/pkg/containerinspector/containerinspector_windows.go
index c9c53558955..e5a2dbc42fb 100644
--- a/pkg/containerinspector/containerinspector_windows.go
+++ b/pkg/containerinspector/containerinspector_windows.go
@@ -19,7 +19,7 @@ package containerinspector
import (
"context"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
func InspectNetNS(ctx context.Context, pid int) (*native.NetNS, error) {
diff --git a/pkg/containerutil/config.go b/pkg/containerutil/config.go
index dfe4ec47e93..4fb5ce45546 100644
--- a/pkg/containerutil/config.go
+++ b/pkg/containerutil/config.go
@@ -24,11 +24,14 @@ import (
"runtime"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/netutil/nettype"
"github.com/opencontainers/runtime-spec/specs-go"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/ipcutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/netutil/nettype"
)
// ReconfigNetContainer reconfigures the container's network namespace path.
@@ -104,3 +107,26 @@ func ReconfigPIDContainer(ctx context.Context, c containerd.Container, client *c
}
return nil
}
+
+// ReconfigIPCContainer reconfigures the container's spec options for sharing IPC namespace and volumns.
+func ReconfigIPCContainer(ctx context.Context, c containerd.Container, client *containerd.Client, lab map[string]string) error {
+ ipc, err := ipcutil.DecodeIPCLabel(lab[labels.IPC])
+ if err != nil {
+ return err
+ }
+ opts, err := ipcutil.GenerateIPCOpts(ctx, ipc, client)
+ if err != nil {
+ return err
+ }
+ spec, err := c.Spec(ctx)
+ if err != nil {
+ return err
+ }
+ err = c.Update(ctx, containerd.UpdateContainerOpts(
+ containerd.WithSpec(spec, oci.Compose(opts...)),
+ ))
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/pkg/containerutil/container_network_manager.go b/pkg/containerutil/container_network_manager.go
index 77e8dfaeabc..23852f56f58 100644
--- a/pkg/containerutil/container_network_manager.go
+++ b/pkg/containerutil/container_network_manager.go
@@ -27,20 +27,23 @@ import (
"runtime"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/netns"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore"
- "github.com/containerd/nerdctl/pkg/idutil/containerwalker"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/mountutil"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/netutil/nettype"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/opencontainers/runtime-spec/specs-go"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/netutil/nettype"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
const (
@@ -83,23 +86,23 @@ func withCustomHosts(src string) func(context.Context, oci.Client, *containers.C
}
}
-// types.NetworkOptionsManager is an interface for reading/setting networking
+// NetworkOptionsManager types.NetworkOptionsManager is an interface for reading/setting networking
// options for containers based on the provided command flags.
type NetworkOptionsManager interface {
- // Returns a copy of the internal types.NetworkOptions.
+ // NetworkOptions Returns a copy of the internal types.NetworkOptions.
NetworkOptions() types.NetworkOptions
- // Verifies that the internal network settings are correct.
+ // VerifyNetworkOptions Verifies that the internal network settings are correct.
VerifyNetworkOptions(context.Context) error
- // Performs setup actions required for the container with the given ID.
+ // SetupNetworking Performs setup actions required for the container with the given ID.
SetupNetworking(context.Context, string) error
- // Performs any required cleanup actions for the given container.
+ // CleanupNetworking Performs any required cleanup actions for the given container.
// Should only be called to revert any setup steps performed in SetupNetworking.
CleanupNetworking(context.Context, containerd.Container) error
- // Returns the set of NetworkingOptions which should be set as labels on the container.
+ // InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container.
//
// These options can potentially differ from the actual networking options
// that the NetworkOptionsManager was initially instantiated with.
@@ -107,13 +110,13 @@ type NetworkOptionsManager interface {
// `--net=container:myContainer` => `--net=container:`.
InternalNetworkingOptionLabels(context.Context) (types.NetworkOptions, error)
- // Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
+ // ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
// the network specs which need to be applied to the container with the given ID.
ContainerNetworkingOpts(context.Context, string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error)
}
-// Returns a types.NetworkOptionsManager based on the provided command's flags.
-func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOpts types.NetworkOptions) (NetworkOptionsManager, error) {
+// NewNetworkingOptionsManager Returns a types.NetworkOptionsManager based on the provided command's flags.
+func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOpts types.NetworkOptions, client *containerd.Client) (NetworkOptionsManager, error) {
netType, err := nettype.Detect(netOpts.NetworkSlice)
if err != nil {
return nil, err
@@ -122,13 +125,17 @@ func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOp
var manager NetworkOptionsManager
switch netType {
case nettype.None:
- manager = &noneNetworkManager{globalOptions, netOpts}
+ manager = &noneNetworkManager{globalOptions, netOpts, client}
case nettype.Host:
- manager = &hostNetworkManager{globalOptions, netOpts}
+ manager = &hostNetworkManager{globalOptions, netOpts, client}
case nettype.Container:
- manager = &containerNetworkManager{globalOptions, netOpts}
+ manager = &containerNetworkManager{globalOptions, netOpts, client}
case nettype.CNI:
- manager = &cniNetworkManager{globalOptions, netOpts, nil}
+ manager = &cniNetworkManager{globalOptions, netOpts, client, cniNetworkManagerPlatform{}}
+ case nettype.Namespace:
+ // We'll handle Namespace networking identically to Host-mode networking, but
+ // put the container in the specified network namespace instead of the root.
+ manager = &hostNetworkManager{globalOptions, netOpts, client}
default:
return nil, fmt.Errorf("unexpected container networking type: %q", netType)
}
@@ -140,36 +147,37 @@ func NewNetworkingOptionsManager(globalOptions types.GlobalCommandOptions, netOp
type noneNetworkManager struct {
globalOptions types.GlobalCommandOptions
netOpts types.NetworkOptions
+ client *containerd.Client
}
-// Returns a copy of the internal types.NetworkOptions.
+// NetworkOptions Returns a copy of the internal types.NetworkOptions.
func (m *noneNetworkManager) NetworkOptions() types.NetworkOptions {
return m.netOpts
}
-// Verifies that the internal network settings are correct.
+// VerifyNetworkOptions Verifies that the internal network settings are correct.
func (m *noneNetworkManager) VerifyNetworkOptions(_ context.Context) error {
// No options to verify if no network settings are provided.
return nil
}
-// Performs setup actions required for the container with the given ID.
+// SetupNetworking Performs setup actions required for the container with the given ID.
func (m *noneNetworkManager) SetupNetworking(_ context.Context, _ string) error {
return nil
}
-// Performs any required cleanup actions for the given container.
+// CleanupNetworking Performs any required cleanup actions for the given container.
// Should only be called to revert any setup steps performed in SetupNetworking.
func (m *noneNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error {
return nil
}
-// Returns the set of NetworkingOptions which should be set as labels on the container.
+// InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container.
func (m *noneNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) {
return m.netOpts, nil
}
-// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
+// ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
// the network specs which need to be applied to the container with the given ID.
func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) {
// No options to return if no network settings are provided.
@@ -180,27 +188,28 @@ func (m *noneNetworkManager) ContainerNetworkingOpts(_ context.Context, _ string
type containerNetworkManager struct {
globalOptions types.GlobalCommandOptions
netOpts types.NetworkOptions
+ client *containerd.Client
}
-// Returns a copy of the internal types.NetworkOptions.
+// NetworkOptions Returns a copy of the internal types.NetworkOptions.
func (m *containerNetworkManager) NetworkOptions() types.NetworkOptions {
return m.netOpts
}
-// Verifies that the internal network settings are correct.
+// VerifyNetworkOptions Verifies that the internal network settings are correct.
func (m *containerNetworkManager) VerifyNetworkOptions(_ context.Context) error {
// TODO: check host OS, not client-side OS.
if runtime.GOOS != "linux" {
return errors.New("container networking mode is currently only supported on Linux")
}
- if m.netOpts.NetworkSlice != nil && len(m.netOpts.NetworkSlice) > 1 {
+ if len(m.netOpts.NetworkSlice) > 1 {
return errors.New("conflicting options: only one network specification is allowed when using '--network=container:'")
}
+ // Note that mac-address is accepted, though it is a no-op
nonZeroParams := nonZeroMapValues(map[string]interface{}{
- "--hostname": m.netOpts.Hostname,
- "--mac-address": m.netOpts.MACAddress,
+ "--hostname": m.netOpts.Hostname,
// NOTE: an empty slice still counts as a non-zero value so we check its length:
"-p/--publish": len(m.netOpts.PortMappings) != 0,
"--dns": len(m.netOpts.DNSServers) != 0,
@@ -225,22 +234,30 @@ func (m *containerNetworkManager) getContainerNetworkFilePaths(containerID strin
if err != nil {
return "", "", "", err
}
+ hostsStore, err := hostsstore.New(dataStore, m.globalOptions.Namespace)
+ if err != nil {
+ return "", "", "", err
+ }
hostnamePath := filepath.Join(conStateDir, "hostname")
resolvConfPath := filepath.Join(conStateDir, "resolv.conf")
- etcHostsPath := hostsstore.HostsPath(dataStore, m.globalOptions.Namespace, containerID)
+
+ etcHostsPath, err := hostsStore.HostsPath(containerID)
+ if err != nil {
+ return "", "", "", err
+ }
return hostnamePath, resolvConfPath, etcHostsPath, nil
}
-// Performs setup actions required for the container with the given ID.
+// SetupNetworking Performs setup actions required for the container with the given ID.
func (m *containerNetworkManager) SetupNetworking(_ context.Context, _ string) error {
// NOTE: container networking simply reuses network config files from the
// bridged container so there are no setup/teardown steps required.
return nil
}
-// Performs any required cleanup actions for the given container.
+// CleanupNetworking Performs any required cleanup actions for the given container.
// Should only be called to revert any setup steps performed in SetupNetworking.
func (m *containerNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error {
// NOTE: container networking simply reuses network config files from the
@@ -249,19 +266,13 @@ func (m *containerNetworkManager) CleanupNetworking(_ context.Context, _ contain
}
// Searches for and returns the networking container for the given network argument.
-func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.Context, containerNetArg string) (containerd.Container, error) {
+func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.Context, containerNetArg string, client *containerd.Client) (containerd.Container, error) {
netItems := strings.Split(containerNetArg, ":")
if len(netItems) < 2 {
return nil, fmt.Errorf("container networking argument format must be 'container:', got: %q", containerNetArg)
}
containerName := netItems[1]
- client, ctxt, cancel, err := clientutil.NewClient(ctx, m.globalOptions.Namespace, m.globalOptions.Address)
- if err != nil {
- return nil, err
- }
- defer cancel()
-
var foundContainer containerd.Container
walker := &containerwalker.ContainerWalker{
Client: client,
@@ -273,7 +284,7 @@ func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.
return nil
},
}
- n, err := walker.Walk(ctxt, containerName)
+ n, err := walker.Walk(ctx, containerName)
if err != nil {
return nil, err
}
@@ -284,14 +295,16 @@ func (m *containerNetworkManager) getNetworkingContainerForArgument(ctx context.
return foundContainer, nil
}
-// Returns the set of NetworkingOptions which should be set as labels on the container.
+// InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container.
func (m *containerNetworkManager) InternalNetworkingOptionLabels(ctx context.Context) (types.NetworkOptions, error) {
opts := m.netOpts
if m.netOpts.NetworkSlice == nil || len(m.netOpts.NetworkSlice) != 1 {
return opts, fmt.Errorf("conflicting options: exactly one network specification is allowed when using '--network=container:'")
}
+ // MacAddress is not allowed with container networking
+ opts.MACAddress = ""
- container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0])
+ container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0], m.client)
if err != nil {
return opts, err
}
@@ -300,13 +313,13 @@ func (m *containerNetworkManager) InternalNetworkingOptionLabels(ctx context.Con
return opts, nil
}
-// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
+// ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
// the network specs which need to be applied to the container with the given ID.
func (m *containerNetworkManager) ContainerNetworkingOpts(ctx context.Context, _ string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) {
opts := []oci.SpecOpts{}
cOpts := []containerd.NewContainerOpts{}
- container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0])
+ container, err := m.getNetworkingContainerForArgument(ctx, m.netOpts.NetworkSlice[0], m.client)
if err != nil {
return nil, nil, err
}
@@ -346,41 +359,96 @@ func (m *containerNetworkManager) ContainerNetworkingOpts(ctx context.Context, _
type hostNetworkManager struct {
globalOptions types.GlobalCommandOptions
netOpts types.NetworkOptions
+ client *containerd.Client
}
-// Returns a copy of the internal types.NetworkOptions.
+// NetworkOptions Returns a copy of the internal types.NetworkOptions.
func (m *hostNetworkManager) NetworkOptions() types.NetworkOptions {
return m.netOpts
}
-// Verifies that the internal network settings are correct.
+// VerifyNetworkOptions Verifies that the internal network settings are correct.
func (m *hostNetworkManager) VerifyNetworkOptions(_ context.Context) error {
// TODO: check host OS, not client-side OS.
if runtime.GOOS == "windows" {
return errors.New("cannot use host networking on Windows")
}
- if m.netOpts.MACAddress != "" {
- return errors.New("conflicting options: mac-address and the network mode")
- }
-
return validateUtsSettings(m.netOpts)
}
-// Performs setup actions required for the container with the given ID.
-func (m *hostNetworkManager) SetupNetworking(_ context.Context, _ string) error {
- // NOTE: there are no setup steps required for host networking.
- return nil
+// SetupNetworking Performs setup actions required for the container with the given ID.
+func (m *hostNetworkManager) SetupNetworking(ctx context.Context, containerID string) error {
+ // Retrieve the container
+ container, err := m.client.ContainerService().Get(ctx, containerID)
+ if err != nil {
+ return err
+ }
+
+ // Get the dataStore
+ dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address)
+ if err != nil {
+ return err
+ }
+
+ // Get the hostsStore
+ hs, err := hostsstore.New(dataStore, container.Labels[labels.Namespace])
+ if err != nil {
+ return err
+ }
+
+ // Get extra-hosts
+ extraHostsJSON := container.Labels[labels.ExtraHosts]
+ var extraHosts []string
+ if err = json.Unmarshal([]byte(extraHostsJSON), &extraHosts); err != nil {
+ return err
+ }
+
+ hosts := make(map[string]string)
+ for _, host := range extraHosts {
+ if v := strings.SplitN(host, ":", 2); len(v) == 2 {
+ hosts[v[0]] = v[1]
+ }
+ }
+
+ // Prep the meta
+ hsMeta := hostsstore.Meta{
+ ID: container.ID,
+ Hostname: container.Labels[labels.Hostname],
+ ExtraHosts: hosts,
+ Name: container.Labels[labels.Name],
+ }
+
+ // Save the meta information
+ return hs.Acquire(hsMeta)
}
-// Performs any required cleanup actions for the given container.
+// CleanupNetworking Performs any required cleanup actions for the given container.
// Should only be called to revert any setup steps performed in SetupNetworking.
-func (m *hostNetworkManager) CleanupNetworking(_ context.Context, _ containerd.Container) error {
- // NOTE: there are no setup steps required for host networking.
- return nil
+func (m *hostNetworkManager) CleanupNetworking(ctx context.Context, container containerd.Container) error {
+ // Get the dataStore
+ dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address)
+ if err != nil {
+ return err
+ }
+
+ // Get labels
+ lbls, err := container.Labels(ctx)
+ if err != nil {
+ return err
+ }
+
+ // Get the hostsStore
+ hs, err := hostsstore.New(dataStore, lbls[labels.Namespace])
+ if err != nil {
+ return err
+ }
+
+ // Release
+ return hs.Release(container.ID())
}
-// Returns the set of NetworkingOptions which should be set as labels on the container.
+// InternalNetworkingOptionLabels Returns the set of NetworkingOptions which should be set as labels on the container.
func (m *hostNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (types.NetworkOptions, error) {
opts := m.netOpts
// Cannot have a MAC address in host networking mode.
@@ -388,25 +456,115 @@ func (m *hostNetworkManager) InternalNetworkingOptionLabels(_ context.Context) (
return opts, nil
}
-// Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
+// withDedupMounts Returns the specOpts if the mountPath is not in existing mounts.
+// for https://github.com/containerd/nerdctl/issues/2685
+func withDedupMounts(mountPath string, defaultSpec oci.SpecOpts) oci.SpecOpts {
+ return func(ctx context.Context, client oci.Client, c *containers.Container, s *oci.Spec) error {
+ for _, m := range s.Mounts {
+ if m.Destination == mountPath {
+ return nil
+ }
+ }
+ return defaultSpec(ctx, client, c, s)
+ }
+}
+
+// copyFileContent copies a file and sets world readable permissions on it, regardless of umask.
+// This is used solely for /etc/resolv.conf and /etc/hosts
+func copyFileContent(src string, dst string) error {
+ data, err := os.ReadFile(src)
+ if err != nil {
+ return err
+ }
+ err = os.WriteFile(dst, data, 0644)
+ if err != nil {
+ return err
+ }
+ err = os.Chmod(dst, 0644)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+// getHostNetworkingNamespace Returns an oci.SpecOpts representing the network namespace to
+// be used by the hostNetworkManager. When running with `--network=host` this would be the host's
+// root namespace, but `--network=ns:` can be used to run a container in an existing netns.
+func getHostNetworkingNamespace(netModeArg string) (oci.SpecOpts, error) {
+ if !strings.Contains(netModeArg, ":") {
+ // Use the host root namespace by default
+ return oci.WithHostNamespace(specs.NetworkNamespace), nil
+ }
+
+ netItems := strings.Split(netModeArg, ":")
+ if len(netItems) < 2 {
+ return nil, fmt.Errorf("namespace networking argument format must be 'ns:', got: %q", netModeArg)
+ }
+ netnsPath := netItems[1]
+ return oci.WithLinuxNamespace(specs.LinuxNamespace{
+ Type: specs.NetworkNamespace,
+ Path: netnsPath,
+ }), nil
+}
+
+// ContainerNetworkingOpts Returns a slice of `oci.SpecOpts` and `containerd.NewContainerOpts` which represent
// the network specs which need to be applied to the container with the given ID.
func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containerID string) ([]oci.SpecOpts, []containerd.NewContainerOpts, error) {
cOpts := []containerd.NewContainerOpts{}
+
+ dataStore, err := clientutil.DataStore(m.globalOptions.DataRoot, m.globalOptions.Address)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ stateDir, err := ContainerStateDirPath(m.globalOptions.Namespace, dataStore, containerID)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ resolvConfPath := filepath.Join(stateDir, "resolv.conf")
+ copyFileContent("/etc/resolv.conf", resolvConfPath)
+
+ hs, err := hostsstore.New(dataStore, m.globalOptions.Namespace)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ content, err := os.ReadFile("/etc/hosts")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ etcHostsPath, err := hs.AllocHostsFile(containerID, content)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ netModeArg := m.netOpts.NetworkSlice[0]
+ netNamespace, err := getHostNetworkingNamespace(netModeArg)
+ if err != nil {
+ return nil, nil, err
+ }
specs := []oci.SpecOpts{
- oci.WithHostNamespace(specs.NetworkNamespace),
- oci.WithHostHostsFile,
- oci.WithHostResolvconf,
+ netNamespace,
+ withDedupMounts("/etc/hosts", withCustomHosts(etcHostsPath)),
+ withDedupMounts("/etc/resolv.conf", withCustomResolvConf(resolvConfPath)),
}
// `/etc/hostname` does not exist on FreeBSD
if runtime.GOOS == "linux" && m.netOpts.UTSNamespace != UtsNamespaceHost {
- // If no hostname is set, default to first 12 characters of the container ID.
hostname := m.netOpts.Hostname
if hostname == "" {
- hostname = containerID
- if len(hostname) > 12 {
- hostname = hostname[0:12]
+ // Hostname by default should be the host hostname
+ hostname, err = os.Hostname()
+ if err != nil {
+ log.L.WithError(err).Warn("could not get hostname")
+ // If no hostname is set, default to first 12 characters of the container ID.
+ hostname = containerID
+ if len(hostname) > 12 {
+ hostname = hostname[0:12]
+ }
}
}
m.netOpts.Hostname = hostname
@@ -420,19 +578,60 @@ func (m *hostNetworkManager) ContainerNetworkingOpts(_ context.Context, containe
}
}
+ if rootlessutil.IsRootless() {
+ detachedNetNS, err := rootlessutil.DetachedNetNS()
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to check whether RootlessKit is running with --detach-netns: %w", err)
+ }
+ if detachedNetNS != "" {
+ // For rootless + host netns, we can't mount sysfs.
+ // We can't (non-recursively) bind mount /sys, either.
+ //
+ // TODO: consider to just rbind /sys from the host with rro,
+ // when rro is available (kernel >= 5.12, runc >= 1.1).
+ //
+ // Relevant: https://github.com/moby/buildkit/blob/v0.12.4/util/rootless/specconv/specconv_linux.go#L15-L34
+ specs = append(specs, withRemoveSysfs)
+ }
+ }
+
return specs, cOpts, nil
}
+func withRemoveSysfs(_ context.Context, _ oci.Client, c *containers.Container, s *oci.Spec) error {
+ var hasSysfs bool
+ for _, mount := range s.Mounts {
+ if mount.Type == "sysfs" {
+ hasSysfs = true
+ break
+ }
+ }
+ if !hasSysfs {
+ // NOP, as the user has specified a custom /sys mount
+ return nil
+ }
+ var mounts []specs.Mount // nolint: prealloc
+ for _, mount := range s.Mounts {
+ if strings.HasPrefix(mount.Destination, "/sys") {
+ continue
+ }
+ mounts = append(mounts, mount)
+ }
+ s.Mounts = mounts
+ return nil
+}
+
// types.NetworkOptionsManager implementation for CNI networking settings.
// This is a more specialized and OS-dependendant networking model so this
// struct provides different implementations on different platforms.
type cniNetworkManager struct {
globalOptions types.GlobalCommandOptions
netOpts types.NetworkOptions
- netNs *netns.NetNS
+ client *containerd.Client
+ cniNetworkManagerPlatform
}
-// Returns a copy of the internal types.NetworkOptions.
+// NetworkOptions Returns a copy of the internal types.NetworkOptions.
func (m *cniNetworkManager) NetworkOptions() types.NetworkOptions {
return m.netOpts
}
@@ -456,6 +655,7 @@ func validateUtsSettings(netOpts types.NetworkOptions) error {
// Nerdctl-managed datastore and returns the oci.SpecOpts required in the container
// spec for the file to be mounted under /etc/hostname in the new container.
// If the hostname is empty, the leading 12 characters of the containerID
+// This sets world readable permissions on /etc/hostname, ignoring umask
func writeEtcHostnameForContainer(globalOptions types.GlobalCommandOptions, hostname string, containerID string) ([]oci.SpecOpts, error) {
if containerID == "" {
return nil, fmt.Errorf("container ID is required for setting up hostname file")
@@ -476,23 +676,26 @@ func writeEtcHostnameForContainer(globalOptions types.GlobalCommandOptions, host
return nil, err
}
+ err = os.Chmod(hostnamePath, 0644)
+ if err != nil {
+ return nil, err
+ }
+
return []oci.SpecOpts{oci.WithHostname(hostname), withCustomEtcHostname(hostnamePath)}, nil
}
// Loads all available networks and verifies that every selected network
// from the networkSlice is of a type within supportedTypes.
+// nolint:unused
func verifyNetworkTypes(env *netutil.CNIEnv, networkSlice []string, supportedTypes []string) (map[string]*netutil.NetworkConfig, error) {
- netMap, err := env.NetworkMap()
- if err != nil {
- return nil, err
- }
-
res := make(map[string]*netutil.NetworkConfig, len(networkSlice))
+ var netConfig *netutil.NetworkConfig
+ var err error
for _, netstr := range networkSlice {
- netConfig, ok := netMap[netstr]
- if !ok {
- return nil, fmt.Errorf("network %s not found", netstr)
+ if netConfig, err = env.NetworkByNameOrID(netstr); err != nil {
+ return nil, err
}
+
netType := netConfig.Plugins[0].Network.Type
if supportedTypes != nil && !strutil.InStringSlice(supportedTypes, netType) {
return nil, fmt.Errorf("network type %q is not supported for network mapping %q, must be one of: %v", netType, netstr, supportedTypes)
@@ -504,7 +707,7 @@ func verifyNetworkTypes(env *netutil.CNIEnv, networkSlice []string, supportedTyp
return res, nil
}
-// Returns the NetworkOptions used in a container's creation from its spec.Annotations.
+// NetworkOptionsFromSpec Returns the NetworkOptions used in a container's creation from its spec.Annotations.
func NetworkOptionsFromSpec(spec *specs.Spec) (types.NetworkOptions, error) {
opts := types.NetworkOptions{}
diff --git a/pkg/containerutil/container_network_manager_linux.go b/pkg/containerutil/container_network_manager_linux.go
index 45bfd4523d0..8b535715ba7 100644
--- a/pkg/containerutil/container_network_manager_linux.go
+++ b/pkg/containerutil/container_network_manager_linux.go
@@ -22,21 +22,25 @@ import (
"io/fs"
"path/filepath"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/clientutil"
- "github.com/containerd/nerdctl/pkg/dnsutil"
- "github.com/containerd/nerdctl/pkg/dnsutil/hostsstore"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/resolvconf"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil"
+ "github.com/containerd/nerdctl/v2/pkg/dnsutil/hostsstore"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/resolvconf"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
+type cniNetworkManagerPlatform struct {
+}
+
// Verifies that the internal network settings are correct.
func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error {
- e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork())
+ e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork(m.globalOptions.BridgeIP))
if err != nil {
return err
}
@@ -95,10 +99,16 @@ func (m *cniNetworkManager) ContainerNetworkingOpts(_ context.Context, container
}
// the content of /etc/hosts is created in OCI Hook
- etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, m.globalOptions.Namespace, containerID)
+ hs, err := hostsstore.New(dataStore, m.globalOptions.Namespace)
if err != nil {
return nil, nil, err
}
+
+ etcHostsPath, err := hs.AllocHostsFile(containerID, []byte(""))
+ if err != nil {
+ return nil, nil, err
+ }
+
opts = append(opts, withCustomResolvConf(resolvConfPath), withCustomHosts(etcHostsPath))
if m.netOpts.UTSNamespace != UtsNamespaceHost {
@@ -149,7 +159,7 @@ func (m *cniNetworkManager) buildResolvConf(resolvConfPath string) error {
}
// if resolvConf file does't exist, using default resolvers
conf = &resolvconf.File{}
- logrus.WithError(err).Debugf("resolvConf file doesn't exist on host")
+ log.L.WithError(err).Debugf("resolvConf file doesn't exist on host")
}
conf, err = resolvconf.FilterResolvDNS(conf.Content, true)
if err != nil {
diff --git a/pkg/containerutil/container_network_manager_other.go b/pkg/containerutil/container_network_manager_other.go
index 500872f57c3..feb97e20f74 100644
--- a/pkg/containerutil/container_network_manager_other.go
+++ b/pkg/containerutil/container_network_manager_other.go
@@ -1,4 +1,4 @@
-//go:build darwin || freebsd || netbsd || openbsd
+//go:build !(linux || windows)
/*
Copyright The containerd Authors.
@@ -23,11 +23,15 @@ import (
"fmt"
"runtime"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/api/types"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
)
+type cniNetworkManagerPlatform struct {
+}
+
// Verifies that the internal network settings are correct.
func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error {
return fmt.Errorf("CNI networking currently unsupported on %s", runtime.GOOS)
diff --git a/pkg/containerutil/container_network_manager_windows.go b/pkg/containerutil/container_network_manager_windows.go
index 88abaeb6073..6cde2b351d8 100644
--- a/pkg/containerutil/container_network_manager_windows.go
+++ b/pkg/containerutil/container_network_manager_windows.go
@@ -20,19 +20,23 @@ import (
"context"
"fmt"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/netns"
- gocni "github.com/containerd/go-cni"
-
- "github.com/containerd/nerdctl/pkg/api/types"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/containerd/nerdctl/pkg/ocihook"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/pkg/netns"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/go-cni"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
+ "github.com/containerd/nerdctl/v2/pkg/ocihook"
)
+type cniNetworkManagerPlatform struct {
+ netNs *netns.NetNS
+}
+
// Verifies that the internal network settings are correct.
func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error {
- e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork())
+ e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork(m.globalOptions.BridgeIP))
if err != nil {
return err
}
@@ -62,26 +66,26 @@ func (m *cniNetworkManager) VerifyNetworkOptions(_ context.Context) error {
return nil
}
-func (m *cniNetworkManager) getCNI() (gocni.CNI, error) {
- e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithDefaultNetwork())
+func (m *cniNetworkManager) getCNI() (cni.CNI, error) {
+ e, err := netutil.NewCNIEnv(m.globalOptions.CNIPath, m.globalOptions.CNINetConfPath, netutil.WithNamespace(m.globalOptions.Namespace), netutil.WithDefaultNetwork(m.globalOptions.BridgeIP))
if err != nil {
return nil, fmt.Errorf("failed to instantiate CNI env: %s", err)
}
- cniOpts := []gocni.Opt{
- gocni.WithPluginDir([]string{m.globalOptions.CNIPath}),
- gocni.WithPluginConfDir(m.globalOptions.CNINetConfPath),
+ cniOpts := []cni.Opt{
+ cni.WithPluginDir([]string{m.globalOptions.CNIPath}),
+ cni.WithPluginConfDir(m.globalOptions.CNINetConfPath),
}
if netMap, err := verifyNetworkTypes(e, m.netOpts.NetworkSlice, nil); err == nil {
for _, netConf := range netMap {
- cniOpts = append(cniOpts, gocni.WithConfListBytes(netConf.Bytes))
+ cniOpts = append(cniOpts, cni.WithConfListBytes(netConf.Bytes))
}
} else {
return nil, err
}
- return gocni.New(cniOpts...)
+ return cni.New(cniOpts...)
}
// Performs setup actions required for the container with the given ID.
@@ -114,12 +118,12 @@ func (m *cniNetworkManager) CleanupNetworking(ctx context.Context, container con
return fmt.Errorf("failed to get container specs for networking cleanup: %s", err)
}
- netNsId, found := spec.Annotations[ocihook.NetworkNamespace]
+ netNsID, found := spec.Annotations[ocihook.NetworkNamespace]
if !found {
return fmt.Errorf("no %q annotation present on container with ID %s", ocihook.NetworkNamespace, containerID)
}
- return cni.Remove(ctx, containerID, netNsId, m.getCNINamespaceOpts()...)
+ return cni.Remove(ctx, containerID, netNsID, m.getCNINamespaceOpts()...)
}
// Returns the set of NetworkingOptions which should be set as labels on the container.
@@ -167,10 +171,10 @@ func (m *cniNetworkManager) setupNetNs() (*netns.NetNS, error) {
return ns, err
}
-// Returns the []gocni.NamespaceOpts to be used for CNI setup/teardown.
-func (m *cniNetworkManager) getCNINamespaceOpts() []gocni.NamespaceOpts {
- opts := []gocni.NamespaceOpts{
- gocni.WithLabels(map[string]string{
+// Returns the []cni.NamespaceOpts to be used for CNI setup/teardown.
+func (m *cniNetworkManager) getCNINamespaceOpts() []cni.NamespaceOpts {
+ opts := []cni.NamespaceOpts{
+ cni.WithLabels(map[string]string{
// allow loose CNI argument verification
// FYI: https://github.com/containernetworking/cni/issues/560
"IgnoreUnknown": "1",
@@ -178,15 +182,15 @@ func (m *cniNetworkManager) getCNINamespaceOpts() []gocni.NamespaceOpts {
}
if m.netOpts.MACAddress != "" {
- opts = append(opts, gocni.WithArgs("MAC", m.netOpts.MACAddress))
+ opts = append(opts, cni.WithArgs("MAC", m.netOpts.MACAddress))
}
if m.netOpts.IPAddress != "" {
- opts = append(opts, gocni.WithArgs("IP", m.netOpts.IPAddress))
+ opts = append(opts, cni.WithArgs("IP", m.netOpts.IPAddress))
}
if m.netOpts.PortMappings != nil {
- opts = append(opts, gocni.WithCapabilityPortMap(m.netOpts.PortMappings))
+ opts = append(opts, cni.WithCapabilityPortMap(m.netOpts.PortMappings))
}
return opts
diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go
index 2e695f02bfd..fca15cb6669 100644
--- a/pkg/containerutil/containerutil.go
+++ b/pkg/containerutil/containerutil.go
@@ -18,6 +18,7 @@ package containerutil
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"io"
@@ -27,24 +28,31 @@ import (
"strings"
"time"
- "github.com/containerd/console"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/runtime/restart"
- "github.com/containerd/nerdctl/pkg/consoleutil"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/formatter"
- "github.com/containerd/nerdctl/pkg/labels"
- "github.com/containerd/nerdctl/pkg/nsutil"
- "github.com/containerd/nerdctl/pkg/portutil"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/signalutil"
- "github.com/containerd/nerdctl/pkg/taskutil"
+ dockercliopts "github.com/docker/cli/opts"
+ dockeropts "github.com/docker/docker/opts"
"github.com/moby/sys/signal"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/console"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/core/runtime/restart"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/consoleutil"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/formatter"
+ "github.com/containerd/nerdctl/v2/pkg/ipcutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels"
+ "github.com/containerd/nerdctl/v2/pkg/portutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/signalutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/taskutil"
)
// PrintHostPort writes to `writer` the public (HostIP:HostPort) of a given `containerPort/protocol` in a container.
@@ -227,6 +235,10 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
return err
}
+ if err := ReconfigIPCContainer(ctx, container, client, lab); err != nil {
+ return err
+ }
+
process, err := container.Spec(ctx)
if err != nil {
return err
@@ -234,7 +246,10 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
flagT := process.Process.Terminal
var con console.Console
if flagA && flagT {
- con = console.Current()
+ con, err = consoleutil.Current()
+ if err != nil {
+ return err
+ }
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
@@ -242,10 +257,10 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
}
logURI := lab[labels.LogURI]
-
+ namespace := lab[labels.Namespace]
cStatus := formatter.ContainerStatus(ctx, container)
if cStatus == "Up" {
- logrus.Warnf("container %s is already running", container.ID())
+ log.G(ctx).Warnf("container %s is already running", container.ID())
return nil
}
@@ -261,11 +276,17 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
}
if oldTask, err := container.Task(ctx, nil); err == nil {
if _, err := oldTask.Delete(ctx); err != nil {
- logrus.WithError(err).Debug("failed to delete old task")
+ log.G(ctx).WithError(err).Debug("failed to delete old task")
}
}
detachC := make(chan struct{})
- task, err := taskutil.NewTask(ctx, client, container, flagA, false, flagT, true, con, logURI, detachKeys, detachC)
+ attachStreamOpt := []string{}
+ if flagA {
+ // In start, flagA attaches only STDOUT/STDERR
+ // source: https://github.com/containerd/nerdctl/blob/main/docs/command-reference.md#whale-nerdctl-start
+ attachStreamOpt = []string{"STDOUT", "STDERR"}
+ }
+ task, err := taskutil.NewTask(ctx, client, container, attachStreamOpt, false, flagT, true, con, logURI, detachKeys, namespace, detachC)
if err != nil {
return err
}
@@ -278,7 +299,7 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie
}
if flagA && flagT {
if err := consoleutil.HandleConsoleResize(ctx, task, con); err != nil {
- logrus.WithError(err).Error("console resize")
+ log.G(ctx).WithError(err).Error("console resize")
}
}
sigc := signalutil.ForwardAllSignals(ctx, task)
@@ -330,9 +351,19 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
if err != nil {
return err
}
+ ipc, err := ipcutil.DecodeIPCLabel(l[labels.IPC])
+ if err != nil {
+ return err
+ }
+ // defer umount
+ defer func() {
+ if err := ipcutil.CleanUp(ipc); err != nil {
+ log.G(ctx).Warnf("failed to clean up IPC container %s: %s", container.ID(), err)
+ }
+ }()
if timeout == nil {
- t, ok := l[labels.StopTimout]
+ t, ok := l[labels.StopTimeout]
if !ok {
// Default is 10 seconds.
t = "10"
@@ -346,6 +377,13 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
task, err := container.Task(ctx, cio.Load)
if err != nil {
+ // NOTE: NotFound doesn't mean that container hasn't started.
+ // In docker/CRI-containerd plugin, the task will be deleted
+ // when it exits. So, the status will be "created" for this
+ // case.
+ if errdefs.IsNotFound(err) {
+ return nil
+ }
return err
}
@@ -389,7 +427,7 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
// signal will be sent once resume is finished
if paused {
if err := task.Resume(ctx); err != nil {
- logrus.Warnf("Cannot unpause container %s: %s", container.ID(), err)
+ log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err)
} else {
// no need to do it again when send sigkill signal
paused = false
@@ -421,7 +459,7 @@ func Stop(ctx context.Context, container containerd.Container, timeout *time.Dur
// signal will be sent once resume is finished
if paused {
if err := task.Resume(ctx); err != nil {
- logrus.Warnf("Cannot unpause container %s: %s", container.ID(), err)
+ log.G(ctx).Warnf("Cannot unpause container %s: %s", container.ID(), err)
}
}
return waitContainerStop(ctx, exitCh, container.ID())
@@ -493,8 +531,112 @@ func Unpause(ctx context.Context, client *containerd.Client, id string) error {
// ContainerStateDirPath returns the path to the Nerdctl-managed state directory for the container with the given ID.
func ContainerStateDirPath(ns, dataStore, id string) (string, error) {
- if err := nsutil.ValidateNamespaceName(ns); err != nil {
- return "", fmt.Errorf("invalid namespace name %q for determining state dir of container %q: %s", ns, id, err)
- }
return filepath.Join(dataStore, "containers", ns, id), nil
}
+
+// ContainerVolume is a struct representing a volume in a container.
+type ContainerVolume struct {
+ Type string
+ Name string
+ Source string
+ Destination string
+ Mode string
+ RW bool
+ Propagation string
+}
+
+// GetContainerVolumes is a function that returns a slice of containerVolume pointers.
+// It accepts a map of container labels as input, where key is the label name and value is its associated value.
+// The function iterates over the predefined volume labels (AnonymousVolumes and Mounts)
+// and for each, it checks if the labels exists in the provided container labels.
+// If yes, it decodes the label value from JSON format and appends the volumes to the result.
+// In case of error during decoding, it logs the error and continues to the next label.
+func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume {
+ var vols []*ContainerVolume
+ volLabels := []string{labels.AnonymousVolumes, labels.Mounts}
+ for _, volLabel := range volLabels {
+ names, ok := containerLabels[volLabel]
+ if !ok {
+ continue
+ }
+ var (
+ volumes []*ContainerVolume
+ err error
+ )
+ if volLabel == labels.Mounts {
+ err = json.Unmarshal([]byte(names), &volumes)
+ }
+ if volLabel == labels.AnonymousVolumes {
+ var anonymous []string
+ err = json.Unmarshal([]byte(names), &anonymous)
+ for _, anony := range anonymous {
+ volumes = append(volumes, &ContainerVolume{Name: anony})
+ }
+
+ }
+ if err != nil {
+ log.L.Warn(err)
+ }
+ vols = append(vols, volumes...)
+ }
+ return vols
+}
+
+func GetContainerName(containerLabels map[string]string) string {
+ if name, ok := containerLabels[labels.Name]; ok {
+ return name
+ }
+
+ if ns, ok := containerLabels[k8slabels.PodNamespace]; ok {
+ if podName, ok := containerLabels[k8slabels.PodName]; ok {
+ if containerName, ok := containerLabels[k8slabels.ContainerName]; ok {
+ // Container
+ return fmt.Sprintf("k8s://%s/%s/%s", ns, podName, containerName)
+ }
+ // Pod sandbox
+ return fmt.Sprintf("k8s://%s/%s", ns, podName)
+ }
+ }
+ return ""
+}
+
+// EncodeContainerRmOptLabel encodes bool value for the --rm option into string value for a label.
+func EncodeContainerRmOptLabel(rmOpt bool) string {
+ return fmt.Sprintf("%t", rmOpt)
+}
+
+// DecodeContainerRmOptLabel decodes bool value for the --rm option from string value for a label.
+func DecodeContainerRmOptLabel(rmOptLabel string) (bool, error) {
+ return strconv.ParseBool(rmOptLabel)
+}
+
+// ParseExtraHosts takes an array of host-to-IP mapping strings, e.g. "localhost:127.0.0.1",
+// and a hostGatewayIP for resolving mappings to "host-gateway".
+//
+// Returns a map of host-to-IPs or errors if any mapping strings are not correctly formatted.
+func ParseExtraHosts(extraHosts []string, hostGatewayIP, separator string) ([]string, error) {
+ hosts := make([]string, 0, len(extraHosts))
+ for _, hostToIP := range strutil.DedupeStrSlice(extraHosts) {
+ if _, err := dockercliopts.ValidateExtraHost(hostToIP); err != nil {
+ return nil, err
+ }
+
+ parts := strings.SplitN(hostToIP, ":", 2)
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("invalid host-to-IP map %s", hostToIP)
+ }
+
+ host, ip := parts[0], parts[1]
+
+ // If the IP address is a string called "host-gateway", replace this value with the IP address stored
+ // in the daemon level HostGatewayIP config variable.
+ if ip == dockeropts.HostGatewayName && hostGatewayIP == "" {
+ return nil, errors.New("unable to derive the IP value for host-gateway")
+ } else if ip == dockeropts.HostGatewayName {
+ ip = hostGatewayIP
+ }
+
+ hosts = append(hosts, host+separator+ip)
+ }
+ return hosts, nil
+}
diff --git a/pkg/containerutil/containerutil_test.go b/pkg/containerutil/containerutil_test.go
new file mode 100644
index 00000000000..88d6c42be94
--- /dev/null
+++ b/pkg/containerutil/containerutil_test.go
@@ -0,0 +1,83 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package containerutil
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestParseExtraHosts(t *testing.T) {
+ tests := []struct {
+ name string
+ extraHosts []string
+ hostGateway string
+ separator string
+ expected []string
+ expectedErrStr string
+ }{
+ {
+ name: "NoExtraHosts",
+ expected: []string{},
+ },
+ {
+ name: "ExtraHosts",
+ extraHosts: []string{"localhost:127.0.0.1", "localhost:[::1]"},
+ separator: ":",
+ expected: []string{"localhost:127.0.0.1", "localhost:[::1]"},
+ },
+ {
+ name: "EqualsSeperator",
+ extraHosts: []string{"localhost:127.0.0.1", "localhost:[::1]"},
+ separator: "=",
+ expected: []string{"localhost=127.0.0.1", "localhost=[::1]"},
+ },
+ {
+ name: "InvalidExtraHostFormat",
+ extraHosts: []string{"localhost"},
+ expectedErrStr: "bad format for add-host: \"localhost\"",
+ },
+ {
+ name: "ErrorOnHostGatewayExtraHostWithNoHostGatewayIPSet",
+ extraHosts: []string{"localhost:host-gateway"},
+ separator: ":",
+ expectedErrStr: "unable to derive the IP value for host-gateway",
+ },
+ {
+ name: "HostGatewayIP",
+ extraHosts: []string{"localhost:host-gateway"},
+ hostGateway: "10.10.0.1",
+ separator: ":",
+ expected: []string{"localhost:10.10.0.1"},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ extraHosts, err := ParseExtraHosts(test.extraHosts, test.hostGateway, test.separator)
+ if err != nil && err.Error() != test.expectedErrStr {
+ t.Fatalf("expected '%s', actual '%v'", test.expectedErrStr, err)
+ } else if err == nil && test.expectedErrStr != "" {
+ t.Fatalf("expected error '%s' but got none", test.expectedErrStr)
+ }
+
+ if !reflect.DeepEqual(test.expected, extraHosts) {
+ t.Fatalf("expected %v, actual %v", test.expected, extraHosts)
+ }
+ })
+ }
+}
diff --git a/pkg/containerutil/cp_linux.go b/pkg/containerutil/cp_linux.go
index 497ab474197..77425aa57be 100644
--- a/pkg/containerutil/cp_linux.go
+++ b/pkg/containerutil/cp_linux.go
@@ -17,7 +17,9 @@
package containerutil
import (
+ "bytes"
"context"
+ "errors"
"fmt"
"os"
"os/exec"
@@ -25,78 +27,190 @@ import (
"strconv"
"strings"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/containerd/nerdctl/pkg/tarutil"
- securejoin "github.com/cyphar/filepath-securejoin"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/tarutil"
)
-// CopyFiles implements `nerdctl cp`.
-//
// See https://docs.docker.com/engine/reference/commandline/cp/ for the specification.
-func CopyFiles(ctx context.Context, container2host bool, pid int, dst, src string, followSymlink bool) error {
- tarBinary, isGNUTar, err := tarutil.FindTarBinary()
+
+var (
+ // Generic and system errors
+ ErrFilesystem = errors.New("filesystem error") // lstat hard errors, etc
+ ErrContainerVanished = errors.New("the container you are trying to copy to/from has been deleted")
+ ErrRootlessCannotCp = errors.New("cannot use cp with stopped containers in rootless mode") // rootless cp with a stopped container
+ ErrFailedMountingSnapshot = errors.New("failed mounting snapshot") // failure to mount a stopped container snapshot
+
+ // CP specific errors
+ ErrTargetIsReadOnly = errors.New("cannot copy into read-only location") // ...
+ ErrSourceIsNotADir = errors.New("source is not a directory") // cp SOMEFILE/ foo:/
+ ErrDestinationIsNotADir = errors.New("destination is not a directory") // * cp ./ foo:/etc/issue/bah
+ ErrSourceDoesNotExist = errors.New("source does not exist") // cp NONEXISTENT foo:/
+ ErrDestinationParentMustExist = errors.New("destination parent does not exist") // nerdctl cp VALID_PATH foo:/NONEXISTENT/NONEXISTENT
+ ErrDestinationDirMustExist = errors.New("the destination directory must exist to be able to copy a file") // * cp SOMEFILE foo:/NONEXISTENT/
+ ErrCannotCopyDirToFile = errors.New("cannot copy a directory to a file") // cp SOMEDIR foo:/etc/issue
+)
+
+// getRoot will tentatively return the root of the container on the host (/proc/pid/root), along with the pid,
+// (eg: doable when the container is running)
+func getRoot(ctx context.Context, container containerd.Container) (string, int, error) {
+ task, err := container.Task(ctx, nil)
if err != nil {
- return err
+ return "", 0, err
}
- logrus.Debugf("Detected tar binary %q (GNU=%v)", tarBinary, isGNUTar)
- var srcFull, dstFull string
- root := fmt.Sprintf("/proc/%d/root", pid)
- if container2host {
- srcFull, err = securejoin.SecureJoin(root, src)
- dstFull = dst
- } else {
- srcFull = src
- dstFull, err = securejoin.SecureJoin(root, dst)
+
+ status, err := task.Status(ctx)
+ if err != nil {
+ return "", 0, err
}
+
+ if status.Status != containerd.Running {
+ return "", 0, nil
+ }
+ pid := int(task.Pid())
+
+ return fmt.Sprintf("/proc/%d/root", pid), pid, nil
+}
+
+// CopyFiles implements `nerdctl cp`
+// It currently depends on the following assumptions:
+// - linux only
+// - tar binary exists on the system
+// - nsenter binary exists on the system
+// - if rootless, the container is running (aka: /proc/pid/root)
+func CopyFiles(ctx context.Context, client *containerd.Client, container containerd.Container, options types.ContainerCpOptions) (err error) {
+ // We do rely on the tar binary as a shortcut - could also be replaced by archive/tar, though that would mean
+ // we need to replace nsenter calls with re-exec
+ tarBinary, isGNUTar, err := tarutil.FindTarBinary()
if err != nil {
return err
}
- var (
- srcIsDir bool
- dstExists bool
- dstExistsAsDir bool
- )
- st, err := os.Stat(srcFull)
+
+ log.G(ctx).Debugf("Detected tar binary %q (GNU=%v)", tarBinary, isGNUTar)
+
+ // This can happen if the container being passed has been deleted since in a racy way
+ conSpec, err := container.Spec(ctx)
if err != nil {
- return err
+ return errors.Join(ErrContainerVanished, err)
}
- srcIsDir = st.IsDir()
- // dst may not exist yet, so err is negligible
- if st, err := os.Stat(dstFull); err == nil {
- dstExists = true
- dstExistsAsDir = st.IsDir()
+ // Try to get a running container root
+ root, pid, err := getRoot(ctx, container)
+ // If the task is "not found" (for example, if the container stopped), we will try to mount the snapshot
+ // Any other type of error from Task() is fatal here.
+ if err != nil && !errdefs.IsNotFound(err) {
+ return errors.Join(ErrContainerVanished, err)
}
- dstEndsWithSep := strings.HasSuffix(dst, string(os.PathSeparator))
- srcEndsWithSlashDot := strings.HasSuffix(src, string(os.PathSeparator)+".")
- if !srcIsDir && dstEndsWithSep && !dstExistsAsDir {
- // The error is specified in https://docs.docker.com/engine/reference/commandline/cp/
- // See the `DEST_PATH does not exist and ends with /` case.
- return fmt.Errorf("the destination directory must exists: %w", err)
+
+ log.G(ctx).Debugf("We have root %s and pid %d", root, pid)
+
+ // If we have no root:
+ // - bail out for rootless
+ // - mount the snapshot for rootful
+ if root == "" {
+ // FIXME: Rootless does not support copying into/out of stopped/created containers as we need to nsenter into
+ // the user namespace of the pid of the running container with --preserve-credentials to preserve uid/gid
+ // mapping and copy files into the container.
+ if rootlessutil.IsRootless() {
+ return ErrRootlessCannotCp
+ }
+
+ // See similar situation above. This may happen if we are racing against container deletion
+ var conInfo containers.Container
+ conInfo, err = container.Info(ctx)
+ if err != nil {
+ return errors.Join(ErrContainerVanished, err)
+ }
+
+ var cleanup func() error
+ root, cleanup, err = mountSnapshotForContainer(ctx, client, conInfo, options.GOptions.Snapshotter)
+ if cleanup != nil {
+ defer func() {
+ err = errors.Join(err, cleanup())
+ }()
+ }
+
+ if err != nil {
+ return errors.Join(ErrFailedMountingSnapshot, err)
+ }
+
+ log.G(ctx).Debugf("Got new root %s", root)
}
- if !srcIsDir && srcEndsWithSlashDot {
- return fmt.Errorf("the source is not a directory")
+
+ var sourceSpec, destinationSpec *pathSpecifier
+ var sourceErr, destErr error
+ if options.Container2Host {
+ sourceSpec, sourceErr = getPathSpecFromContainer(options.SrcPath, conSpec, root)
+ destinationSpec, destErr = getPathSpecFromHost(options.DestPath)
+ } else {
+ sourceSpec, sourceErr = getPathSpecFromHost(options.SrcPath)
+ destinationSpec, destErr = getPathSpecFromContainer(options.DestPath, conSpec, root)
}
- if srcIsDir && dstExists && !dstExistsAsDir {
- return fmt.Errorf("cannot copy a directory to a file")
+
+ if destErr != nil {
+ if errors.Is(destErr, errDoesNotExist) {
+ return ErrDestinationParentMustExist
+ } else if errors.Is(destErr, errIsNotADir) {
+ return ErrDestinationIsNotADir
+ }
+
+ return errors.Join(ErrFilesystem, destErr)
}
- if srcIsDir && !dstExists {
- if err := os.MkdirAll(dstFull, 0755); err != nil {
- return err
+
+ if sourceErr != nil {
+ if errors.Is(sourceErr, errDoesNotExist) {
+ return ErrSourceDoesNotExist
+ } else if errors.Is(sourceErr, errIsNotADir) {
+ return ErrSourceIsNotADir
+ }
+
+ return errors.Join(ErrFilesystem, sourceErr)
+ }
+
+ // Now, resolve cp shenanigans
+ // First, cannot copy a non-existent resource
+ if !sourceSpec.exists {
+ return ErrSourceDoesNotExist
+ }
+
+ // Second, cannot copy into a readonly destination
+ if destinationSpec.readOnly {
+ return ErrTargetIsReadOnly
+ }
+
+ // Cannot copy a dir into a file
+ if sourceSpec.isADir && destinationSpec.exists && !destinationSpec.isADir {
+ return ErrCannotCopyDirToFile
+ }
+
+ // A file cannot be copied inside a non-existent directory with a trailing slash, or slash+dot
+ if !sourceSpec.isADir && !destinationSpec.exists && (destinationSpec.endsWithSeparator || destinationSpec.endsWithSeparatorDot) {
+ return ErrDestinationDirMustExist
+ }
+
+ // XXX FIXME: this seems wrong. What about ownership? We could be doing that inside a container
+ if !destinationSpec.exists {
+ if err = os.Mkdir(destinationSpec.resolvedPath, 0o755); err != nil {
+ return errors.Join(ErrFilesystem, err)
}
}
var tarCDir, tarCArg string
- if srcIsDir {
- if !dstExists || srcEndsWithSlashDot {
+ if sourceSpec.isADir {
+ if !destinationSpec.exists || sourceSpec.endsWithSeparatorDot {
// the content of the source directory is copied into this directory
- tarCDir = srcFull
+ tarCDir = sourceSpec.resolvedPath
tarCArg = "."
} else {
// the source directory is copied into this directory
- tarCDir = filepath.Dir(srcFull)
- tarCArg = filepath.Base(srcFull)
+ tarCDir = filepath.Dir(sourceSpec.resolvedPath)
+ tarCArg = filepath.Base(sourceSpec.resolvedPath)
}
} else {
// Prepare a single-file directory to create an archive of the source file
@@ -107,46 +221,50 @@ func CopyFiles(ctx context.Context, container2host bool, pid int, dst, src strin
defer os.RemoveAll(td)
tarCDir = td
cp := []string{"cp", "-a"}
- if followSymlink {
+ if options.FollowSymLink {
cp = append(cp, "-L")
}
- if dstEndsWithSep || dstExistsAsDir {
- tarCArg = filepath.Base(srcFull)
+ if destinationSpec.endsWithSeparator || (destinationSpec.exists && destinationSpec.isADir) {
+ tarCArg = filepath.Base(sourceSpec.resolvedPath)
} else {
// Handle `nerdctl cp /path/to/file some-container:/path/to/file-with-another-name`
- tarCArg = filepath.Base(dstFull)
+ tarCArg = filepath.Base(destinationSpec.resolvedPath)
}
- cp = append(cp, srcFull, filepath.Join(td, tarCArg))
+ cp = append(cp, sourceSpec.resolvedPath, filepath.Join(td, tarCArg))
cpCmd := exec.CommandContext(ctx, cp[0], cp[1:]...)
- logrus.Debugf("executing %v", cpCmd.Args)
+ log.G(ctx).Debugf("executing %v", cpCmd.Args)
if out, err := cpCmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to execute %v: %w (out=%q)", cpCmd.Args, err, string(out))
}
}
tarC := []string{tarBinary}
- if followSymlink {
+ if options.FollowSymLink {
tarC = append(tarC, "-h")
}
tarC = append(tarC, "-c", "-f", "-", tarCArg)
- tarXDir := dstFull
- if !srcIsDir && !dstEndsWithSep && !dstExistsAsDir {
- tarXDir = filepath.Dir(dstFull)
+ tarXDir := destinationSpec.resolvedPath
+ if !sourceSpec.isADir && !destinationSpec.endsWithSeparator && !(destinationSpec.exists && destinationSpec.isADir) {
+ tarXDir = filepath.Dir(destinationSpec.resolvedPath)
}
tarX := []string{tarBinary, "-x"}
- if container2host && isGNUTar {
+ if options.Container2Host && isGNUTar {
tarX = append(tarX, "--no-same-owner")
}
tarX = append(tarX, "-f", "-")
+
if rootlessutil.IsRootless() {
nsenter := []string{"nsenter", "-t", strconv.Itoa(pid), "-U", "--preserve-credentials", "--"}
- if container2host {
+ if options.Container2Host {
tarC = append(nsenter, tarC...)
} else {
tarX = append(nsenter, tarX...)
}
}
+ // FIXME: moving to archive/tar should allow better error management than this
+ // WARNING: some of our testing on stderr might not be portable across different versions of tar
+ // In these cases (readonly target), we will just get the straight tar output instead
tarCCmd := exec.CommandContext(ctx, tarC[0], tarC[1:]...)
tarCCmd.Dir = tarCDir
tarCCmd.Stdin = nil
@@ -159,21 +277,74 @@ func CopyFiles(ctx context.Context, container2host bool, pid int, dst, src strin
return err
}
tarXCmd.Stdout = os.Stderr
- tarXCmd.Stderr = os.Stderr
+ var tarErr bytes.Buffer
+ tarXCmd.Stderr = &tarErr
- logrus.Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir)
+ log.G(ctx).Debugf("executing %v in %q", tarCCmd.Args, tarCCmd.Dir)
if err := tarCCmd.Start(); err != nil {
- return fmt.Errorf("failed to execute %v: %w", tarCCmd.Args, err)
+ return errors.Join(fmt.Errorf("failed to execute %v", tarCCmd.Args), err)
}
- logrus.Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir)
+
+ log.G(ctx).Debugf("executing %v in %q", tarXCmd.Args, tarXCmd.Dir)
if err := tarXCmd.Start(); err != nil {
- return fmt.Errorf("failed to execute %v: %w", tarXCmd.Args, err)
+ if strings.Contains(err.Error(), "permission denied") {
+ return ErrTargetIsReadOnly
+ }
+
+ // Other errors, just put them back on stderr
+ _, fpErr := fmt.Fprint(os.Stderr, tarErr.String())
+ if fpErr != nil {
+ return errors.Join(fpErr, err)
+ }
+
+ return errors.Join(fmt.Errorf("failed to execute %v", tarXCmd.Args), err)
}
+
if err := tarCCmd.Wait(); err != nil {
return fmt.Errorf("failed to wait %v: %w", tarCCmd.Args, err)
}
+
if err := tarXCmd.Wait(); err != nil {
- return fmt.Errorf("failed to wait %v: %w", tarXCmd.Args, err)
+ if strings.Contains(tarErr.String(), "Read-only file system") {
+ return ErrTargetIsReadOnly
+ }
+
+ // Other errors, just put them back on stderr
+ _, fpErr := fmt.Fprint(os.Stderr, tarErr.String())
+ if fpErr != nil {
+ return errors.Join(fpErr, err)
+ }
+
+ return errors.Join(fmt.Errorf("failed to wait %v", tarXCmd.Args), err)
}
+
return nil
}
+
+func mountSnapshotForContainer(ctx context.Context, client *containerd.Client, conInfo containers.Container, snapshotter string) (string, func() error, error) {
+ snapKey := conInfo.SnapshotKey
+ resp, err := client.SnapshotService(snapshotter).Mounts(ctx, snapKey)
+ if err != nil {
+ return "", nil, err
+ }
+
+ tempDir, err := os.MkdirTemp("", "nerdctl-cp-")
+ if err != nil {
+ return "", nil, err
+ }
+
+ err = mount.All(resp, tempDir)
+ if err != nil {
+ return "", nil, err
+ }
+
+ cleanup := func() error {
+ err = mount.Unmount(tempDir, 0)
+ if err != nil {
+ return err
+ }
+ return os.RemoveAll(tempDir)
+ }
+
+ return tempDir, cleanup, nil
+}
diff --git a/pkg/containerutil/cp_resolve_linux.go b/pkg/containerutil/cp_resolve_linux.go
new file mode 100644
index 00000000000..ab22abaf38e
--- /dev/null
+++ b/pkg/containerutil/cp_resolve_linux.go
@@ -0,0 +1,444 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package containerutil
+
+import (
+ "errors"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "runtime"
+ "slices"
+ "strings"
+ "syscall"
+
+ "github.com/opencontainers/runtime-spec/specs-go"
+
+ "github.com/containerd/containerd/v2/pkg/oci"
+)
+
+// volumeNameLen returns length of the leading volume name on Windows.
+// It returns 0 elsewhere.
+// FIXME: whenever we will want to port cp to windows, we will need the windows implementation of volumeNameLen
+func volumeNameLen(_ string) int {
+ return 0
+}
+
+var (
+ errDoesNotExist = errors.New("resource does not exist") // when a path parent dir does not exist
+ errIsNotADir = errors.New("is not a dir") // when a path is a file, ending with path separator
+ errCannotResolvePathNoCwd = errors.New("unable to resolve path against undefined current working directory") // relative host path, no cwd
+)
+
+// pathSpecifier represents a path to be used by cp
+// besides exposing relevant properties (endsWithSeparator, etc), it also provides a fully resolved *host* path to
+// access the resource
+type pathSpecifier struct {
+ originalPath string
+ endsWithSeparator bool
+ endsWithSeparatorDot bool
+ exists bool
+ isADir bool
+ readOnly bool
+ resolvedPath string
+}
+
+// getPathSpecFromHost builds a pathSpecifier from a host location
+// errors with errDoesNotExist, errIsNotADir, "EvalSymlinks: too many links", or other hard filesystem errors from lstat/stat
+func getPathSpecFromHost(originalPath string) (*pathSpecifier, error) {
+ pathSpec := &pathSpecifier{
+ originalPath: originalPath,
+ endsWithSeparator: strings.HasSuffix(originalPath, string(os.PathSeparator)),
+ endsWithSeparatorDot: filepath.Base(originalPath) == ".",
+ }
+
+ path := originalPath
+
+ // Path may still be relative at this point. If it is, figure out getwd.
+ if !filepath.IsAbs(path) {
+ cwd, err := os.Getwd()
+ if err != nil {
+ return nil, errors.Join(errCannotResolvePathNoCwd, err)
+ }
+ path = cwd + string(os.PathSeparator) + path
+ }
+
+ // Try to fully resolve the path
+ resolvedPath, err := filepath.EvalSymlinks(path)
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ if errors.Is(err, syscall.ENOTDIR) {
+ return nil, errors.Join(errIsNotADir, err)
+ }
+
+ // Other errors:
+ // - "EvalSymlinks: too many links"
+ // - any other error coming from lstat
+ return nil, err
+ }
+
+ pathSpec.exists = err == nil
+
+ // Ensure the parent exists if the path itself does not
+ if !pathSpec.exists {
+ // Try the parent - obtain it by removing any trailing / or /., then the base
+ cleaned := strings.TrimRight(strings.TrimSuffix(path, string(os.PathSeparator)+"."), string(os.PathSeparator))
+ for len(cleaned) < len(path) {
+ path = cleaned
+ cleaned = strings.TrimRight(strings.TrimSuffix(path, string(os.PathSeparator)+"."), string(os.PathSeparator))
+ }
+
+ base := filepath.Base(path)
+ path = strings.TrimSuffix(path, string(os.PathSeparator)+base)
+
+ // Resolve it
+ resolvedPath, err = filepath.EvalSymlinks(path)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, errors.Join(errDoesNotExist, err)
+ } else if errors.Is(err, syscall.ENOTDIR) {
+ return nil, errors.Join(errIsNotADir, err)
+ }
+
+ return nil, err
+ }
+
+ resolvedPath = filepath.Join(resolvedPath, base)
+ } else {
+ // If it exists, we can check if it is a dir
+ var st os.FileInfo
+ st, err = os.Stat(path)
+ if err != nil {
+ return nil, err
+ }
+ pathSpec.isADir = st.IsDir()
+ }
+
+ pathSpec.resolvedPath = resolvedPath
+
+ return pathSpec, nil
+}
+
+// getPathSpecFromHost builds a pathSpecifier from a container location
+func getPathSpecFromContainer(originalPath string, conSpec *oci.Spec, containerHostRoot string) (*pathSpecifier, error) {
+ pathSpec := &pathSpecifier{
+ originalPath: originalPath,
+ endsWithSeparator: strings.HasSuffix(originalPath, string(os.PathSeparator)),
+ endsWithSeparatorDot: filepath.Base(originalPath) == ".",
+ }
+
+ path := originalPath
+
+ // Path may still be relative at this point. If it is, join it to the root
+ // NOTE: this is specifically called out in the docker reference. Paths in the container are assumed
+ // relative to the root, and not to the current (container) working directory.
+ // Though this seems like a questionable decision, it is set.
+ if !filepath.IsAbs(path) {
+ path = string(os.PathSeparator) + path
+ }
+
+ // Now, fully resolve the path - resolving all symlinks and cleaning-up the end result, following across mounts
+ pathResolver := newResolver(conSpec, containerHostRoot)
+ resolvedContainerPath, err := pathResolver.resolvePath(path)
+
+ // Errors we get from that are from Lstat or Readlink
+ // Either the object does not exist, or we have a dangling symlink, or otherwise hosed filesystem entries
+ if err != nil && !errors.Is(err, os.ErrNotExist) {
+ if errors.Is(err, syscall.ENOTDIR) {
+ return nil, errors.Join(errIsNotADir, err)
+ }
+
+ // errors.New("EvalSymlinks: too many links")
+ // other errors would come from lstat
+ return nil, err
+ }
+
+ pathSpec.exists = err == nil
+
+ // If the resource does not exist
+ if !pathSpec.exists {
+ // Try the parent
+ cleaned := strings.TrimRight(strings.TrimSuffix(path, string(os.PathSeparator)+"."), string(os.PathSeparator))
+ for len(cleaned) < len(path) {
+ path = cleaned
+ cleaned = strings.TrimRight(strings.TrimSuffix(path, string(os.PathSeparator)+"."), string(os.PathSeparator))
+ }
+
+ base := filepath.Base(path)
+ path = strings.TrimSuffix(path, string(os.PathSeparator)+base)
+
+ resolvedContainerPath, err = pathResolver.resolvePath(path)
+
+ // Error? That is the end
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, errors.Join(errDoesNotExist, err)
+ } else if errors.Is(err, syscall.ENOTDIR) {
+ return nil, errors.Join(errIsNotADir, err)
+ }
+
+ return nil, err
+ }
+
+ resolvedContainerPath = filepath.Join(resolvedContainerPath, base)
+ }
+
+ // Now, finally get the location of the fully resolved containerPath (in the root? in a volume?)
+ containerMount, relativePath := pathResolver.getMount(resolvedContainerPath)
+ pathSpec.resolvedPath = filepath.Join(containerMount.hostPath, relativePath)
+ // If the endpoint is readonly, flag it as such
+ if containerMount.readonly {
+ pathSpec.readOnly = true
+ }
+
+ // If it exists, we can check if it is a dir
+ if pathSpec.exists {
+ var st os.FileInfo
+ st, err = os.Stat(pathSpec.resolvedPath)
+ if err != nil {
+ return nil, err
+ }
+
+ pathSpec.isADir = st.IsDir()
+ }
+
+ return pathSpec, nil
+}
+
+// resolver provides methods to fully resolve any given container given path to a host location
+// accounting for rootfs and mounts location
+type resolver struct {
+ root *specs.Root
+ mounts []specs.Mount
+ hostRoot string
+}
+
+// locator represents a container mount
+type locator struct {
+ containerPath string
+ hostPath string
+ readonly bool
+}
+
+func isParent(child []string, candidate []string) (bool, []string) {
+ if len(child) < len(candidate) {
+ return false, child
+ }
+ return slices.Equal(child[0:len(candidate)], candidate), child[len(candidate):]
+}
+
+// newResolver returns a resolver struct
+func newResolver(conSpec *oci.Spec, hostRoot string) *resolver {
+ return &resolver{
+ root: conSpec.Root,
+ mounts: conSpec.Mounts,
+ hostRoot: hostRoot,
+ }
+}
+
+// pathOnHost will return the *host* location of a container path, accounting for volumes.
+// The provided path must be fully resolved, as returned by `resolvePath`.
+func (res *resolver) pathOnHost(path string) string {
+ hostRoot := res.hostRoot
+ path = filepath.Clean(path)
+ itemized := strings.Split(path, string(os.PathSeparator))
+
+ containerRoot := "/"
+ sub := itemized
+
+ for _, mnt := range res.mounts {
+ if candidateIsParent, subPath := isParent(itemized, strings.Split(mnt.Destination, string(os.PathSeparator))); candidateIsParent {
+ if len(mnt.Destination) > len(containerRoot) {
+ containerRoot = mnt.Destination
+ hostRoot = mnt.Source
+ sub = subPath
+ }
+ }
+ }
+
+ return filepath.Join(append([]string{hostRoot}, sub...)...)
+}
+
+// getMount returns the mount locator for a given fully-resolved path, along with the corresponding subpath of the path
+// relative to the locator
+func (res *resolver) getMount(path string) (*locator, string) {
+ itemized := strings.Split(path, string(os.PathSeparator))
+
+ loc := &locator{
+ containerPath: "/",
+ hostPath: res.hostRoot,
+ readonly: res.root.Readonly,
+ }
+
+ sub := itemized
+
+ for _, mnt := range res.mounts {
+ if candidateIsParent, subPath := isParent(itemized, strings.Split(mnt.Destination, string(os.PathSeparator))); candidateIsParent {
+ if len(mnt.Destination) > len(loc.containerPath) {
+ loc.readonly = false
+ for _, option := range mnt.Options {
+ if option == "ro" {
+ loc.readonly = true
+ }
+ }
+ loc.containerPath = mnt.Destination
+ loc.hostPath = mnt.Source
+ sub = subPath
+ }
+ }
+ }
+
+ return loc, filepath.Join(sub...)
+}
+
+// resolvePath is adapted from https://cs.opensource.google/go/go/+/go1.23.0:src/path/filepath/path.go;l=147
+// The (only) changes are on Lstat and ReadLink, which are fed the actual host path, that is computed by `res.pathOnHost`
+func (res *resolver) resolvePath(path string) (string, error) {
+ volLen := volumeNameLen(path)
+ pathSeparator := string(os.PathSeparator)
+
+ if volLen < len(path) && os.IsPathSeparator(path[volLen]) {
+ volLen++
+ }
+ vol := path[:volLen]
+ dest := vol
+ linksWalked := 0
+ //nolint:ineffassign
+ for start, end := volLen, volLen; start < len(path); start = end {
+ for start < len(path) && os.IsPathSeparator(path[start]) {
+ start++
+ }
+ end = start
+ for end < len(path) && !os.IsPathSeparator(path[end]) {
+ end++
+ }
+
+ // On Windows, "." can be a symlink.
+ // We look it up, and use the value if it is absolute.
+ // If not, we just return ".".
+ //nolint:staticcheck
+ isWindowsDot := runtime.GOOS == "windows" && path[volumeNameLen(path):] == "."
+
+ // The next path component is in path[start:end].
+ if end == start {
+ // No more path components.
+ break
+ } else if path[start:end] == "." && !isWindowsDot {
+ // Ignore path component ".".
+ continue
+ } else if path[start:end] == ".." {
+ // Back up to previous component if possible.
+ // Note that volLen includes any leading slash.
+
+ // Set r to the index of the last slash in dest,
+ // after the volume.
+ var r int
+ for r = len(dest) - 1; r >= volLen; r-- {
+ if os.IsPathSeparator(dest[r]) {
+ break
+ }
+ }
+ if r < volLen || dest[r+1:] == ".." {
+ // Either path has no slashes
+ // (it's empty or just "C:")
+ // or it ends in a ".." we had to keep.
+ // Either way, keep this "..".
+ if len(dest) > volLen {
+ dest += pathSeparator
+ }
+ dest += ".."
+ } else {
+ // Discard everything since the last slash.
+ dest = dest[:r]
+ }
+ continue
+ }
+
+ // Ordinary path component. Add it to result.
+
+ if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
+ dest += pathSeparator
+ }
+
+ dest += path[start:end]
+
+ // Resolve symlink.
+ hostPath := res.pathOnHost(dest)
+ fi, err := os.Lstat(hostPath)
+ if err != nil {
+ return "", err
+ }
+
+ if fi.Mode()&fs.ModeSymlink == 0 {
+ if !fi.Mode().IsDir() && end < len(path) {
+ return "", syscall.ENOTDIR
+ }
+ continue
+ }
+
+ // Found symlink.
+ linksWalked++
+ if linksWalked > 255 {
+ return "", errors.New("EvalSymlinks: too many links")
+ }
+
+ link, err := os.Readlink(hostPath)
+ if err != nil {
+ return "", err
+ }
+
+ if isWindowsDot && !filepath.IsAbs(link) {
+ // On Windows, if "." is a relative symlink,
+ // just return ".".
+ break
+ }
+
+ path = link + path[end:]
+
+ v := volumeNameLen(link)
+ if v > 0 {
+ // Symlink to drive name is an absolute path.
+ if v < len(link) && os.IsPathSeparator(link[v]) {
+ v++
+ }
+ vol = link[:v]
+ dest = vol
+ end = len(vol)
+ } else if len(link) > 0 && os.IsPathSeparator(link[0]) {
+ // Symlink to absolute path.
+ dest = link[:1]
+ end = 1
+ vol = link[:1]
+ volLen = 1
+ } else {
+ // Symlink to relative path; replace last
+ // path component in dest.
+ var r int
+ for r = len(dest) - 1; r >= volLen; r-- {
+ if os.IsPathSeparator(dest[r]) {
+ break
+ }
+ }
+ if r < volLen {
+ dest = vol
+ } else {
+ dest = dest[:r]
+ }
+ end = 0
+ }
+ }
+ return filepath.Clean(dest), nil
+}
diff --git a/pkg/containerutil/lock.go b/pkg/containerutil/lock.go
new file mode 100644
index 00000000000..228b8790575
--- /dev/null
+++ b/pkg/containerutil/lock.go
@@ -0,0 +1,37 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package containerutil
+
+import (
+ "path/filepath"
+
+ "github.com/containerd/nerdctl/v2/pkg/store"
+)
+
+func Lock(stateDir string) (store.Store, error) {
+ stor, err := store.New(filepath.Join(stateDir, "oplock"), 0, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ err = stor.Lock()
+ if err != nil {
+ return nil, err
+ }
+
+ return stor, nil
+}
diff --git a/pkg/defaults/cgroup_linux.go b/pkg/defaults/cgroup_linux.go
index caaae236e61..41e64cece16 100644
--- a/pkg/defaults/cgroup_linux.go
+++ b/pkg/defaults/cgroup_linux.go
@@ -20,7 +20,8 @@ import (
"os"
"github.com/containerd/cgroups/v3"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
func IsSystemdAvailable() bool {
diff --git a/pkg/defaults/defaults_darwin.go b/pkg/defaults/defaults_darwin.go
new file mode 100644
index 00000000000..38db7823db1
--- /dev/null
+++ b/pkg/defaults/defaults_darwin.go
@@ -0,0 +1,45 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// This is a dummy file to allow usage of library functions
+// on Darwin-based systems.
+// All functions and variables are empty/no-ops
+
+package defaults
+
+func CNIPath() string {
+ return ""
+}
+
+func CNINetConfPath() string {
+ return ""
+}
+
+func DataRoot() string {
+ return ""
+}
+
+func CgroupManager() string {
+ return ""
+}
+
+func HostsDirs() []string {
+ return []string{}
+}
+
+func HostGatewayIP() string {
+ return ""
+}
diff --git a/pkg/defaults/defaults_freebsd.go b/pkg/defaults/defaults_freebsd.go
index 620006a5e9a..8092beb8585 100644
--- a/pkg/defaults/defaults_freebsd.go
+++ b/pkg/defaults/defaults_freebsd.go
@@ -17,11 +17,14 @@
package defaults
import (
- gocni "github.com/containerd/go-cni"
+ "github.com/containerd/go-cni"
)
-const AppArmorProfileName = ""
-const Runtime = "wtf.sbk.runj.v1"
+const (
+ AppArmorProfileName = ""
+ SeccompProfileName = ""
+ Runtime = "wtf.sbk.runj.v1"
+)
func DataRoot() string {
return "/var/lib/nerdctl"
@@ -29,21 +32,17 @@ func DataRoot() string {
func CNIPath() string {
// default: /opt/cni/bin
- return gocni.DefaultCNIDir
+ return cni.DefaultCNIDir
}
func CNINetConfPath() string {
- return gocni.DefaultNetDir
+ return cni.DefaultNetDir
}
func CNIRuntimeDir() string {
return "/run/cni"
}
-func BuildKitHost() string {
- return "unix:///run/buildkit/buildkitd.sock"
-}
-
func CgroupManager() string {
return ""
}
diff --git a/pkg/defaults/defaults_linux.go b/pkg/defaults/defaults_linux.go
index b5a6a462f0a..ac35cf9c786 100644
--- a/pkg/defaults/defaults_linux.go
+++ b/pkg/defaults/defaults_linux.go
@@ -22,14 +22,18 @@ import (
"os"
"path/filepath"
- "github.com/containerd/containerd/plugin"
- gocni "github.com/containerd/go-cni"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/plugins"
+ "github.com/containerd/go-cni"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
-const AppArmorProfileName = "nerdctl-default"
-const Runtime = plugin.RuntimeRuncV2
+const (
+ AppArmorProfileName = "nerdctl-default"
+ SeccompProfileName = "builtin"
+ Runtime = plugins.RuntimeRuncV2
+)
func DataRoot() string {
if !rootlessutil.IsRootless() {
@@ -44,6 +48,7 @@ func DataRoot() string {
func CNIPath() string {
candidates := []string{
+ cni.DefaultCNIDir, // /opt/cni/bin
"/usr/local/libexec/cni",
"/usr/local/lib/cni",
"/usr/libexec/cni", // Fedora
@@ -56,9 +61,9 @@ func CNIPath() string {
}
candidates = append([]string{
// NOTE: These user paths are not defined in XDG
+ filepath.Join(home, "opt/cni/bin"),
filepath.Join(home, ".local/libexec/cni"),
filepath.Join(home, ".local/lib/cni"),
- filepath.Join(home, "opt/cni/bin"),
}, candidates...)
}
@@ -69,12 +74,12 @@ func CNIPath() string {
}
// default: /opt/cni/bin
- return gocni.DefaultCNIDir
+ return cni.DefaultCNIDir
}
func CNINetConfPath() string {
if !rootlessutil.IsRootless() {
- return gocni.DefaultNetDir
+ return cni.DefaultNetDir
}
xch, err := rootlessutil.XDGConfigHome()
if err != nil {
@@ -89,24 +94,12 @@ func CNIRuntimeDir() string {
}
xdr, err := rootlessutil.XDGRuntimeDir()
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
xdr = fmt.Sprintf("/run/user/%d", rootlessutil.ParentEUID())
}
return fmt.Sprintf("%s/cni", xdr)
}
-func BuildKitHost() string {
- if !rootlessutil.IsRootless() {
- return "unix:///run/buildkit/buildkitd.sock"
- }
- xdr, err := rootlessutil.XDGRuntimeDir()
- if err != nil {
- logrus.Warn(err)
- xdr = fmt.Sprintf("/run/user/%d", rootlessutil.ParentEUID())
- }
- return fmt.Sprintf("unix://%s/buildkit/buildkitd.sock", xdr)
-}
-
func NerdctlTOML() string {
if !rootlessutil.IsRootless() {
return "/etc/nerdctl/nerdctl.toml"
@@ -134,6 +127,7 @@ func HostsDirs() []string {
// HostGatewayIP returns the non-loop-back host ip if available and returns empty string if running into error.
func HostGatewayIP() string {
+ // no need to use [rootlessutil.WithDetachedNetNSIfAny] here
addrs, err := net.InterfaceAddrs()
if err != nil {
return ""
diff --git a/pkg/defaults/defaults_windows.go b/pkg/defaults/defaults_windows.go
index 7c4a7929dfe..65d74d2c8bb 100644
--- a/pkg/defaults/defaults_windows.go
+++ b/pkg/defaults/defaults_windows.go
@@ -17,13 +17,15 @@
package defaults
import (
- "fmt"
"os"
"path/filepath"
)
-const AppArmorProfileName = ""
-const Runtime = "io.containerd.runhcs.v1"
+const (
+ AppArmorProfileName = ""
+ SeccompProfileName = ""
+ Runtime = "io.containerd.runhcs.v1"
+)
func DataRoot() string {
return filepath.Join(os.Getenv("ProgramData"), "nerdctl")
@@ -41,10 +43,6 @@ func CNIRuntimeDir() string {
return ""
}
-func BuildKitHost() string {
- return fmt.Sprint("\\\\.\\pipe\\buildkit")
-}
-
func IsSystemdAvailable() bool {
return false
}
diff --git a/pkg/dnsutil/dnsutil.go b/pkg/dnsutil/dnsutil.go
index acbfc68c7c4..433a19b324b 100644
--- a/pkg/dnsutil/dnsutil.go
+++ b/pkg/dnsutil/dnsutil.go
@@ -19,7 +19,7 @@ package dnsutil
import (
"context"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
)
func GetSlirp4netnsDNS() ([]string, error) {
diff --git a/pkg/dnsutil/hostsstore/hostsstore.go b/pkg/dnsutil/hostsstore/hostsstore.go
index f90e7ad8752..1980e4f934b 100644
--- a/pkg/dnsutil/hostsstore/hostsstore.go
+++ b/pkg/dnsutil/hostsstore/hostsstore.go
@@ -14,91 +14,69 @@
limitations under the License.
*/
-// Package hostsstore provides the interface for /var/lib/nerdctl//etchosts .
-// Prioritize simplicity over scalability.
+// Package hostsstore provides the interface for /var/lib/nerdctl//etchosts
+// Prioritizes simplicity over scalability.
+// All methods perform atomic writes and are safe to use concurrently.
+// Note that locking is done per namespace.
+// hostsstore is currently by container rename, remove, network managers, and ocihooks
+// Finally, NOTE:
+// Since we will write to the hosts file after it is mounted in the container, we cannot use our atomic write method
+// as the inode would change on rename.
+// Henceforth, hosts file mutation uses filesystem methods instead, making it the one exception that has to bypass
+// the Store implementation.
package hostsstore
import (
+ "bytes"
"encoding/json"
"errors"
+ "fmt"
"os"
"path/filepath"
+ "strings"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/lockutil"
types100 "github.com/containernetworking/cni/pkg/types/100"
+
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/store"
)
const (
// hostsDirBasename is the base name of /var/lib/nerdctl//etchosts
hostsDirBasename = "etchosts"
- // metaJSON is stored as /var/lib/nerdctl//etchosts///meta.json
+ // metaJSON is stored as hostsDirBasename///meta.json
metaJSON = "meta.json"
+ // hostsFile is stored as hostsDirBasename///hosts
+ hostsFile = "hosts"
)
-// HostsPath returns "/var/lib/nerdctl//etchosts///hosts"
-func HostsPath(dataStore, ns, id string) string {
- if dataStore == "" || ns == "" || id == "" {
- panic(errdefs.ErrInvalidArgument)
- }
- return filepath.Join(dataStore, hostsDirBasename, ns, id, "hosts")
-}
+// ErrHostsStore will wrap all errors here
+var ErrHostsStore = errors.New("hosts-store error")
-// ensureFile ensures a file with permission 0644.
-// The file is initialized with no content.
-// The dir (if not exists) is created with permission 0700.
-func ensureFile(path string) error {
- if path == "" {
- return errdefs.ErrInvalidArgument
- }
- dir := filepath.Dir(path)
- if err := os.MkdirAll(dir, 0700); err != nil {
- return err
- }
- f, err := os.OpenFile(path, os.O_CREATE, 0644)
- if err != nil {
- f.Close()
- }
- return err
-}
+func New(dataStore string, namespace string) (retStore Store, err error) {
+ defer func() {
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
-// AllocHostsFile is used for creating mount-bindable /etc/hosts file.
-// The file is initialized with no content.
-func AllocHostsFile(dataStore, ns, id string) (string, error) {
- lockDir := filepath.Join(dataStore, hostsDirBasename)
- if err := os.MkdirAll(lockDir, 0700); err != nil {
- return "", err
+ if dataStore == "" || namespace == "" {
+ return nil, store.ErrInvalidArgument
}
- path := HostsPath(dataStore, ns, id)
- fn := func() error {
- return ensureFile(path)
- }
- err := lockutil.WithDirLock(lockDir, fn)
- return path, err
-}
-func DeallocHostsFile(dataStore, ns, id string) error {
- lockDir := filepath.Join(dataStore, hostsDirBasename)
- if err := os.MkdirAll(lockDir, 0700); err != nil {
- return err
- }
- dirToBeRemoved := filepath.Dir(HostsPath(dataStore, ns, id))
- fn := func() error {
- return os.RemoveAll(dirToBeRemoved)
+ st, err := store.New(filepath.Join(dataStore, hostsDirBasename, namespace), 0, 0o644)
+ if err != nil {
+ return nil, err
}
- return lockutil.WithDirLock(lockDir, fn)
-}
-func NewStore(dataStore string) (Store, error) {
- store := &store{
- dataStore: dataStore,
- hostsD: filepath.Join(dataStore, hostsDirBasename),
- }
- return store, os.MkdirAll(store.hostsD, 0700)
+ return &hostsStore{
+ safeStore: st,
+ }, nil
}
type Meta struct {
- Namespace string
ID string
Networks map[string]*types100.Result
Hostname string
@@ -108,76 +86,270 @@ type Meta struct {
type Store interface {
Acquire(Meta) error
- Release(ns, id string) error
- Update(ns, id, newName string) error
+ Release(id string) error
+ Update(id, newName string) error
+ HostsPath(id string) (location string, err error)
+ Delete(id string) (err error)
+ AllocHostsFile(id string, content []byte) (location string, err error)
}
-type store struct {
- // dataStore is /var/lib/nerdctl/
- dataStore string
- // hostsD is /var/lib/nerdctl//etchosts
- hostsD string
+type hostsStore struct {
+ safeStore store.Store
}
-func (x *store) Acquire(meta Meta) error {
- fn := func() error {
- hostsPath := HostsPath(x.dataStore, meta.Namespace, meta.ID)
- if err := ensureFile(hostsPath); err != nil {
+func (x *hostsStore) Acquire(meta Meta) (err error) {
+ defer func() {
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
+
+ return x.safeStore.WithLock(func() error {
+ var loc string
+ loc, err = x.safeStore.Location(meta.ID, hostsFile)
+ if err != nil {
return err
}
- metaB, err := json.Marshal(meta)
+
+ if err = os.WriteFile(loc, []byte{}, 0o644); err != nil {
+ return errors.Join(store.ErrSystemFailure, err)
+ }
+
+ // os.WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched
+ // against the current process umask.
+ // See https://www.man7.org/linux/man-pages/man2/open.2.html for details.
+ // Since we must make sure that these files are world readable, explicitly chmod them here.
+ if err = os.Chmod(loc, 0o644); err != nil {
+ err = errors.Join(store.ErrSystemFailure, err)
+ }
+
+ var content []byte
+ content, err = json.Marshal(meta)
if err != nil {
return err
}
- metaPath := filepath.Join(x.hostsD, meta.Namespace, meta.ID, metaJSON)
- if err := os.WriteFile(metaPath, metaB, 0644); err != nil {
+
+ if err = x.safeStore.Set(content, meta.ID, metaJSON); err != nil {
return err
}
- return newUpdater(meta.ID, x.hostsD, meta.ExtraHosts).update()
- }
- return lockutil.WithDirLock(x.hostsD, fn)
+
+ return x.updateAllHosts()
+ })
}
// Release is triggered by Poststop hooks.
// It is called after the containerd task is deleted but before the delete operation returns.
-func (x *store) Release(ns, id string) error {
- fn := func() error {
- metaPath := filepath.Join(x.hostsD, ns, id, metaJSON)
- if _, err := os.Stat(metaPath); errors.Is(err, os.ErrNotExist) {
- return nil
- }
- // We remove "meta.json" but we still retain the "hosts" file
- // because it is needed for restarting. The "hosts" is removed on
- // `nerdctl rm`.
- // https://github.com/rootless-containers/rootlesskit/issues/220#issuecomment-783224610
- if err := os.RemoveAll(metaPath); err != nil {
+func (x *hostsStore) Release(id string) (err error) {
+ // We remove "meta.json" but we still retain the "hosts" file
+ // because it is needed for restarting. The "hosts" is removed on
+ // `nerdctl rm`.
+ // https://github.com/rootless-containers/rootlesskit/issues/220#issuecomment-783224610
+ defer func() {
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
+
+ return x.safeStore.WithLock(func() error {
+ if err = x.safeStore.Delete(id, metaJSON); err != nil {
+ return err
+ }
+
+ return x.updateAllHosts()
+ })
+}
+
+// AllocHostsFile is used for creating mount-bindable /etc/hosts file.
+func (x *hostsStore) AllocHostsFile(id string, content []byte) (location string, err error) {
+ defer func() {
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
+
+ err = x.safeStore.WithLock(func() error {
+ err = x.safeStore.GroupEnsure(id)
+ if err != nil {
+ return err
+ }
+
+ var loc string
+ loc, err = x.safeStore.Location(id, hostsFile)
+ if err != nil {
return err
}
- return newUpdater(id, x.hostsD, nil).update()
+
+ err = os.WriteFile(loc, content, 0o644)
+ if err != nil {
+ err = errors.Join(store.ErrSystemFailure, err)
+ }
+
+ // os.WriteFile relies on syscall.Open. Unless there are ACLs, the effective mode of the file will be matched
+ // against the current process umask.
+ // See https://www.man7.org/linux/man-pages/man2/open.2.html for details.
+ // Since we must make sure that these files are world readable, explicitly chmod them here.
+ if err = os.Chmod(loc, 0o644); err != nil {
+ err = errors.Join(store.ErrSystemFailure, err)
+ }
+
+ return err
+ })
+ if err != nil {
+ return "", err
+ }
+
+ return x.safeStore.Location(id, hostsFile)
+}
+
+func (x *hostsStore) Delete(id string) (err error) {
+ err = x.safeStore.WithLock(func() error { return x.safeStore.Delete(id) })
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
}
- return lockutil.WithDirLock(x.hostsD, fn)
+
+ return err
+}
+
+func (x *hostsStore) HostsPath(id string) (location string, err error) {
+ defer func() {
+ if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
+
+ return x.safeStore.Location(id, hostsFile)
}
-func (x *store) Update(ns, id, newName string) error {
- fn := func() error {
- metaPath := filepath.Join(x.hostsD, ns, id, metaJSON)
- metaB, err := os.ReadFile(metaPath)
+func (x *hostsStore) Update(id, newName string) (err error) {
+ defer func() {
if err != nil {
+ err = errors.Join(ErrHostsStore, err)
+ }
+ }()
+
+ return x.safeStore.WithLock(func() error {
+ var content []byte
+ if content, err = x.safeStore.Get(id, metaJSON); err != nil {
return err
}
+
meta := &Meta{}
- if err := json.Unmarshal(metaB, meta); err != nil {
+ if err = json.Unmarshal(content, meta); err != nil {
return err
}
+
meta.Name = newName
- metaB, err = json.Marshal(meta)
+ content, err = json.Marshal(meta)
if err != nil {
return err
}
- if err := os.WriteFile(metaPath, metaB, 0644); err != nil {
+
+ if err = x.safeStore.Set(content, id, metaJSON); err != nil {
return err
}
- return newUpdater(meta.ID, x.hostsD, meta.ExtraHosts).update()
+
+ return x.updateAllHosts()
+ })
+}
+
+func (x *hostsStore) updateAllHosts() (err error) {
+ entries, err := x.safeStore.List()
+ if err != nil {
+ return err
+ }
+
+ metasByEntry := map[string]*Meta{}
+ metasByIP := map[string]*Meta{}
+ networkNameByIP := map[string]string{}
+
+ // Phase 1: read all meta files
+ for _, entry := range entries {
+ var content []byte
+ content, err = x.safeStore.Get(entry, metaJSON)
+ if err != nil {
+ log.L.WithError(err).Debugf("unable to read %q", entry)
+ continue
+ }
+ meta := &Meta{}
+ if err = json.Unmarshal(content, meta); err != nil {
+ log.L.WithError(err).Warnf("unable to unmarshell %q", entry)
+ continue
+ }
+ metasByEntry[entry] = meta
+
+ for netName, cniRes := range meta.Networks {
+ for _, ipCfg := range cniRes.IPs {
+ if ip := ipCfg.Address.IP; ip != nil {
+ if ip.IsLoopback() || ip.IsUnspecified() {
+ continue
+ }
+ ipStr := ip.String()
+ metasByIP[ipStr] = meta
+ networkNameByIP[ipStr] = netName
+ }
+ }
+ }
+ }
+
+ // Phase 2: write hosts files
+ for _, entry := range entries {
+ myMeta, ok := metasByEntry[entry]
+ if !ok {
+ log.L.WithError(errdefs.ErrNotFound).Debugf("hostsstore metadata %q not found in %q?", metaJSON, entry)
+ continue
+ }
+
+ myNetworks := make(map[string]struct{})
+ for nwName := range myMeta.Networks {
+ myNetworks[nwName] = struct{}{}
+ }
+
+ var content []byte
+ content, err = x.safeStore.Get(entry, hostsFile)
+ if err != nil {
+ log.L.WithError(err).Errorf("unable to retrieve the hosts file for %q", entry)
+ continue
+ }
+
+ // parse the hosts file, keep the original host record
+ // retain custom /etc/hosts entries outside region
+ var buf bytes.Buffer
+ if content != nil {
+ if err = parseHostsButSkipMarkedRegion(&buf, bytes.NewReader(content)); err != nil {
+ log.L.WithError(err).Errorf("failed to read hosts file for %q", entry)
+ continue
+ }
+ }
+
+ buf.WriteString(fmt.Sprintf("# %s\n", MarkerBegin))
+ buf.WriteString("127.0.0.1 localhost localhost.localdomain\n")
+ buf.WriteString("::1 localhost localhost.localdomain\n")
+
+ // keep extra hosts first
+ for host, ip := range myMeta.ExtraHosts {
+ buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, host))
+ }
+
+ for ip, netName := range networkNameByIP {
+ meta := metasByIP[ip]
+ if line := createLine(netName, meta, myNetworks); len(line) != 0 {
+ buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, strings.Join(line, " ")))
+ }
+ }
+
+ buf.WriteString(fmt.Sprintf("# %s\n", MarkerEnd))
+
+ var loc string
+ loc, err = x.safeStore.Location(entry, hostsFile)
+ if err != nil {
+ return err
+ }
+
+ err = os.WriteFile(loc, buf.Bytes(), 0o644)
+ if err != nil {
+ log.L.WithError(err).Errorf("failed to write hosts file for %q", entry)
+ }
+ _ = os.Chmod(loc, 0o644)
}
- return lockutil.WithDirLock(x.hostsD, fn)
+ return nil
}
diff --git a/pkg/dnsutil/hostsstore/updater.go b/pkg/dnsutil/hostsstore/updater.go
index 51dd9768939..2b77bb8301d 100644
--- a/pkg/dnsutil/hostsstore/updater.go
+++ b/pkg/dnsutil/hostsstore/updater.go
@@ -17,157 +17,9 @@
package hostsstore
import (
- "bytes"
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "strings"
-
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/nerdctl/pkg/netutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/nerdctl/v2/pkg/netutil"
)
-// newUpdater creates an updater for hostsD (/var/lib/nerdctl//etchosts)
-func newUpdater(id, hostsD string, extraHosts map[string]string) *updater {
- u := &updater{
- id: id,
- hostsD: hostsD,
- metaByIPStr: make(map[string]*Meta),
- nwNameByIPStr: make(map[string]string),
- metaByDir: make(map[string]*Meta),
- extraHosts: extraHosts,
- }
- return u
-}
-
-// updater is the struct for updater.update()
-type updater struct {
- id string
- hostsD string // "/var/lib/nerdctl//etchosts"
- metaByIPStr map[string]*Meta // key: IP string
- nwNameByIPStr map[string]string // key: IP string, value: key of Meta.Networks
- metaByDir map[string]*Meta // key: "/var/lib/nerdctl//etchosts//"
- extraHosts map[string]string // key: host value: IP string
-}
-
-// update updates the hostsD tree.
-// Must be called with a locker for the hostsD directory.
-func (u *updater) update() error {
- // phase1: read meta.json
- if err := u.phase1(); err != nil {
- return err
- }
- // phase2: write hosts
- if err := u.phase2(); err != nil {
- return err
- }
- return nil
-}
-
-// phase1: read meta.json
-func (u *updater) phase1() error {
- readMetaWF := func(path string, _ os.FileInfo, walkErr error) error {
- if walkErr != nil {
- return walkErr
- }
- if filepath.Base(path) != metaJSON {
- return nil
- }
- metaB, err := os.ReadFile(path)
- if err != nil {
- return err
- }
- var meta Meta
- if err := json.Unmarshal(metaB, &meta); err != nil {
- return err
- }
- u.metaByDir[filepath.Dir(path)] = &meta
- for nwName, cniRes := range meta.Networks {
- for _, ipCfg := range cniRes.IPs {
- if ip := ipCfg.Address.IP; ip != nil {
- if ip.IsLoopback() || ip.IsUnspecified() {
- continue
- }
- ipStr := ip.String()
- u.metaByIPStr[ipStr] = &meta
- u.nwNameByIPStr[ipStr] = nwName
- }
- }
- }
- return nil
- }
- if err := filepath.Walk(u.hostsD, readMetaWF); err != nil {
- return err
- }
- return nil
-}
-
-// phase2: write hosts
-func (u *updater) phase2() error {
- writeHostsWF := func(path string, _ os.FileInfo, walkErr error) error {
- if walkErr != nil {
- return walkErr
- }
- if filepath.Base(path) != "hosts" {
- return nil
- }
- dir := filepath.Dir(path)
- myMeta, ok := u.metaByDir[dir]
- if !ok {
- logrus.WithError(errdefs.ErrNotFound).Debugf("hostsstore metadata %q not found in %q?", metaJSON, dir)
- return nil
- }
- myNetworks := make(map[string]struct{})
- for nwName := range myMeta.Networks {
- myNetworks[nwName] = struct{}{}
- }
-
- // parse the hosts file, keep the original host record
- // retain custom /etc/hosts entries outside region
- r, err := os.Open(path)
- if err != nil {
- return err
- }
- var buf bytes.Buffer
- if r != nil {
- if err := parseHostsButSkipMarkedRegion(&buf, r); err != nil {
- logrus.WithError(err).Warn("failed to read hosts file")
- }
- }
-
- buf.WriteString(fmt.Sprintf("# %s\n", MarkerBegin))
- buf.WriteString("127.0.0.1 localhost localhost.localdomain\n")
- buf.WriteString("::1 localhost localhost.localdomain\n")
-
- // keep extra hosts first
- if u.id == myMeta.ID {
- for host, ip := range u.extraHosts {
- buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, host))
- }
- }
-
- for ip, nwName := range u.nwNameByIPStr {
- meta := u.metaByIPStr[ip]
- if line := createLine(nwName, meta, myNetworks); len(line) != 0 {
- buf.WriteString(fmt.Sprintf("%-15s %s\n", ip, strings.Join(line, " ")))
- }
- }
-
- buf.WriteString(fmt.Sprintf("# %s\n", MarkerEnd))
- err = os.WriteFile(path, buf.Bytes(), 0644)
- if err != nil {
- return err
- }
- return nil
- }
- if err := filepath.Walk(u.hostsD, writeHostsWF); err != nil {
- return err
- }
- return nil
-}
-
// createLine returns a line string slice.
// line is like "foo foo.nw0 bar bar.nw0\n"
// for `nerdctl --name=foo --hostname=bar --network=n0`.
diff --git a/pkg/dnsutil/hostsstore/updater_test.go b/pkg/dnsutil/hostsstore/updater_test.go
index c55f4ae3cd6..03ffefae12b 100644
--- a/pkg/dnsutil/hostsstore/updater_test.go
+++ b/pkg/dnsutil/hostsstore/updater_test.go
@@ -74,8 +74,7 @@ func TestCreateLine(t *testing.T) {
}
for _, tc := range testCases {
thatMeta := &Meta{
- Namespace: "default",
- ID: "984d63ce45ae",
+ ID: "984d63ce45ae",
Networks: map[string]*types100.Result{
tc.thatNetwork: {
Interfaces: []*types100.Interface{
diff --git a/pkg/errutil/errors_check.go b/pkg/errutil/errors_check.go
index 755fbf2582e..202c4fe8518 100644
--- a/pkg/errutil/errors_check.go
+++ b/pkg/errutil/errors_check.go
@@ -18,15 +18,6 @@ package errutil
import "strings"
-// IsErrHTTPResponseToHTTPSClient returns whether err is
-// "http: server gave HTTP response to HTTPS client"
-func IsErrHTTPResponseToHTTPSClient(err error) bool {
- // The error string is unexposed as of Go 1.16, so we can't use `errors.Is`.
- // https://github.com/golang/go/issues/44855
- const unexposed = "server gave HTTP response to HTTPS client"
- return strings.Contains(err.Error(), unexposed)
-}
-
// IsErrConnectionRefused return whether err is
// "connect: connection refused"
func IsErrConnectionRefused(err error) bool {
diff --git a/pkg/eventutil/eventutil.go b/pkg/eventutil/eventutil.go
index fa4c7d8afe2..a624332e80d 100644
--- a/pkg/eventutil/eventutil.go
+++ b/pkg/eventutil/eventutil.go
@@ -19,7 +19,7 @@ package eventutil
import (
"sync"
- "github.com/containerd/containerd/events"
+ "github.com/containerd/containerd/v2/core/events"
)
type eventHandler struct {
diff --git a/pkg/flagutil/flagutil.go b/pkg/flagutil/flagutil.go
index 064b8a8ab1a..663a5746161 100644
--- a/pkg/flagutil/flagutil.go
+++ b/pkg/flagutil/flagutil.go
@@ -23,7 +23,7 @@ import (
"os"
"strings"
- "github.com/containerd/nerdctl/pkg/strutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
// ReplaceOrAppendEnvValues returns the defaults with the overrides either
diff --git a/pkg/formatter/common.go b/pkg/formatter/common.go
index 8915f0dc3ec..18cd6e6ca45 100644
--- a/pkg/formatter/common.go
+++ b/pkg/formatter/common.go
@@ -42,11 +42,16 @@ func FormatSlice(format string, writer io.Writer, x []interface{}) error {
var tmpl *template.Template
switch format {
case "":
- b, err := json.MarshalIndent(x, "", " ")
+ // Avoid escaping "<", ">", "&"
+ // https://pkg.go.dev/encoding/json
+ encoder := json.NewEncoder(writer)
+ encoder.SetIndent("", " ")
+ encoder.SetEscapeHTML(false)
+ err := encoder.Encode(x)
if err != nil {
return err
}
- fmt.Fprintln(writer, string(b))
+ fmt.Fprint(writer, "\n")
case "raw", "table", "wide":
return errors.New("unsupported format: \"raw\", \"table\", and \"wide\"")
default:
@@ -65,7 +70,7 @@ func FormatSlice(format string, writer io.Writer, x []interface{}) error {
}
}
}
- if _, err = fmt.Fprintf(writer, b.String()+"\n"); err != nil {
+ if _, err = fmt.Fprintln(writer, b.String()); err != nil {
return err
}
}
diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go
index d0dc761a5c9..1c6acdcfced 100644
--- a/pkg/formatter/formatter.go
+++ b/pkg/formatter/formatter.go
@@ -25,16 +25,17 @@ import (
"strings"
"time"
+ "github.com/docker/go-units"
"golang.org/x/text/cases"
"golang.org/x/text/language"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/runtime/restart"
- "github.com/containerd/nerdctl/pkg/portutil"
- "github.com/docker/go-units"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/runtime/restart"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/portutil"
)
func ContainerStatus(ctx context.Context, c containerd.Container) string {
@@ -118,7 +119,7 @@ func Ellipsis(str string, maxDisplayWidth int) string {
func FormatPorts(labelMap map[string]string) string {
ports, err := portutil.ParsePortsLabel(labelMap)
if err != nil {
- logrus.Error(err.Error())
+ log.L.Error(err.Error())
}
if len(ports) == 0 {
return ""
diff --git a/pkg/identifiers/validate.go b/pkg/identifiers/validate.go
new file mode 100644
index 00000000000..7f5ff3522d3
--- /dev/null
+++ b/pkg/identifiers/validate.go
@@ -0,0 +1,46 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+// Package identifiers implements functions for docker compatible identifier validation.
+package identifiers
+
+import (
+ "fmt"
+ "regexp"
+
+ "github.com/containerd/errdefs"
+)
+
+const AllowedIdentfierChars = `[a-zA-Z0-9][a-zA-Z0-9_.-]`
+
+var AllowedIdentifierPattern = regexp.MustCompile(`^` + AllowedIdentfierChars + `+$`)
+
+// ValidateDockerCompat implements docker compatible identifier validation.
+// The containerd implementation allows single character identifiers, while the
+// Docker compatible implementation requires at least 2 characters for identifiers.
+// The containerd implementation enforces a maximum length constraint of 76 characters,
+// while the Docker compatible implementation omits the length check entirely.
+func ValidateDockerCompat(s string) error {
+ if len(s) == 0 {
+ return fmt.Errorf("identifier must not be empty %w", errdefs.ErrInvalidArgument)
+ }
+
+ if !AllowedIdentifierPattern.MatchString(s) {
+ return fmt.Errorf("identifier %q must match pattern %q: %w", s, AllowedIdentfierChars, errdefs.ErrInvalidArgument)
+ }
+
+ return nil
+}
diff --git a/pkg/idutil/containerwalker/containerwalker.go b/pkg/idutil/containerwalker/containerwalker.go
index d90896e894b..83e9926046f 100644
--- a/pkg/idutil/containerwalker/containerwalker.go
+++ b/pkg/idutil/containerwalker/containerwalker.go
@@ -22,8 +22,9 @@ import (
"regexp"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/nerdctl/pkg/labels"
+ containerd "github.com/containerd/containerd/v2/client"
+
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
type Found struct {
diff --git a/pkg/idutil/imagewalker/imagewalker.go b/pkg/idutil/imagewalker/imagewalker.go
index a67f9e58051..c99cdb8b432 100644
--- a/pkg/idutil/imagewalker/imagewalker.go
+++ b/pkg/idutil/imagewalker/imagewalker.go
@@ -22,25 +22,47 @@ import (
"regexp"
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/nerdctl/pkg/referenceutil"
"github.com/opencontainers/go-digest"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
type Found struct {
- Image images.Image
- Req string // The raw request string. name, short ID, or long ID.
- MatchIndex int // Begins with 0, up to MatchCount - 1.
- MatchCount int // 1 on exact match. > 1 on ambiguous match. Never be <= 0.
- UniqueImages int // Number of unique images in all found images.
+ Image images.Image
+ Req string // The raw request string. name, short ID, or long ID.
+ MatchIndex int // Begins with 0, up to MatchCount - 1.
+ MatchCount int // 1 on exact match. > 1 on ambiguous match. Never be <= 0.
+ UniqueImages int // Number of unique images in all found images.
+ NameMatchIndex int // Image index with a name matching the argument for `nerdctl rmi`.
}
type OnFound func(ctx context.Context, found Found) error
+/*
+In order to resolve the issue with OnFoundCriRm, the same imageId under
+k8s.io is showing multiple results: repo:tag, repo:digest, configID. We expect
+to display only repo:tag, consistent with other namespaces and CRI.
+e.g.
+
+ nerdctl -n k8s.io images
+ REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE
+ centos 7 be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ centos be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+ be65f488b776 3 hours ago linux/amd64 211.5 MiB 72.6 MiB
+
+The boolean value will return true only when the repo:tag is successfully
+deleted for each image. Once all repo:tag entries are deleted, it is necessary
+to clean up the remaining repo:digest and configID.
+*/
+type OnFoundCriRm func(ctx context.Context, found Found) (bool, error)
+
type ImageWalker struct {
- Client *containerd.Client
- OnFound OnFound
+ Client *containerd.Client
+ OnFound OnFound
+ OnFoundCriRm OnFoundCriRm
}
// Walk walks images and calls w.OnFound .
@@ -48,8 +70,12 @@ type ImageWalker struct {
// Returns the number of the found entries.
func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
var filters []string
- if canonicalRef, err := referenceutil.ParseAny(req); err == nil {
- filters = append(filters, fmt.Sprintf("name==%s", canonicalRef.String()))
+ var parsedReferenceStr string
+
+ parsedReference, err := referenceutil.Parse(req)
+ if err == nil {
+ parsedReferenceStr = parsedReference.String()
+ filters = append(filters, fmt.Sprintf("name==%s", parsedReferenceStr))
}
filters = append(filters,
fmt.Sprintf("name==%s", req),
@@ -66,17 +92,23 @@ func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
// to handle the `rmi -f` case where returned images are different but
// have the same short prefix.
uniqueImages := make(map[digest.Digest]bool)
- for _, image := range images {
+ nameMatchIndex := -1
+ for i, image := range images {
uniqueImages[image.Target.Digest] = true
+ // to get target image index for `nerdctl rmi `.
+ if (parsedReferenceStr != "" && image.Name == parsedReferenceStr) || image.Name == req {
+ nameMatchIndex = i
+ }
}
for i, img := range images {
f := Found{
- Image: img,
- Req: req,
- MatchIndex: i,
- MatchCount: matchCount,
- UniqueImages: len(uniqueImages),
+ Image: img,
+ Req: req,
+ MatchIndex: i,
+ MatchCount: matchCount,
+ UniqueImages: len(uniqueImages),
+ NameMatchIndex: nameMatchIndex,
}
if e := w.OnFound(ctx, f); e != nil {
return -1, e
@@ -85,6 +117,103 @@ func (w *ImageWalker) Walk(ctx context.Context, req string) (int, error) {
return matchCount, nil
}
+// WalkCriRm walks images and calls w.OnFoundCriRm .
+// Only effective when in the k8s.io namespace and kube-hide-dupe is enabled.
+// The WalkCriRm deletes non-repo:tag items such as repo:digest when in the no-other-repo:tag scenario.
+func (w *ImageWalker) WalkCriRm(ctx context.Context, req string) (int, error) {
+ var filters []string
+ var parsedReferenceStr, repo string
+ var imageTag, imagesRepo []images.Image
+ var tagNum int
+
+ parsedReference, err := referenceutil.Parse(req)
+ if err == nil {
+ parsedReferenceStr = parsedReference.String()
+ filters = append(filters, fmt.Sprintf("name==%s", parsedReferenceStr))
+ }
+ //Get the image ID , if reg == imageTag use
+ image, err := w.Client.GetImage(ctx, parsedReferenceStr)
+ if err != nil {
+ repo = req
+ } else {
+ repo = strings.Split(image.Target().Digest.String(), ":")[1][:12]
+ }
+
+ filters = append(filters,
+ fmt.Sprintf("name==%s", req),
+ fmt.Sprintf("target.digest~=^sha256:%s.*$", regexp.QuoteMeta(repo)),
+ fmt.Sprintf("target.digest~=^%s.*$", regexp.QuoteMeta(repo)),
+ )
+
+ images, err := w.Client.ImageService().List(ctx, filters...)
+ if err != nil {
+ return -1, err
+ }
+
+ // to handle the `rmi -f` case where returned images are different but
+ // have the same short prefix.
+ uniqueImages := make(map[digest.Digest]bool)
+ nameMatchIndex := -1
+
+ //Distinguish between tag and non-tag
+ for _, img := range images {
+ ref := img.Name
+ parsed, err := referenceutil.Parse(ref)
+ if err != nil {
+ continue
+ }
+ if parsed.Tag != "" {
+ imageTag = append(imageTag, img)
+ tagNum++
+ uniqueImages[img.Target.Digest] = true
+ // to get target image index for `nerdctl rmi `.
+ if (parsedReferenceStr != "" && img.Name == parsedReferenceStr) || img.Name == req {
+ nameMatchIndex = len(imageTag) - 1
+ }
+ } else {
+ imagesRepo = append(imagesRepo, img)
+ }
+ }
+
+ matchCount := len(imageTag)
+ if matchCount < 1 && len(imagesRepo) > 0 {
+ matchCount = 1
+ }
+
+ for i, img := range imageTag {
+ f := Found{
+ Image: img,
+ Req: req,
+ MatchIndex: i,
+ MatchCount: matchCount,
+ UniqueImages: len(uniqueImages),
+ NameMatchIndex: nameMatchIndex,
+ }
+ if ok, e := w.OnFoundCriRm(ctx, f); e != nil {
+ return -1, e
+ } else if ok {
+ tagNum = tagNum - 1
+ }
+ }
+ //If the corresponding imageTag does not exist, delete the repoDigests
+ if tagNum == 0 {
+ for i, img := range imagesRepo {
+ f := Found{
+ Image: img,
+ Req: req,
+ MatchIndex: i,
+ MatchCount: 1,
+ UniqueImages: 1,
+ NameMatchIndex: -1,
+ }
+ if _, e := w.OnFoundCriRm(ctx, f); e != nil {
+ return -1, e
+ }
+ }
+ }
+ return matchCount, nil
+}
+
// WalkAll calls `Walk` for each req in `reqs`.
//
// It can be used when the matchCount is not important (e.g., only care if there
diff --git a/pkg/idutil/netwalker/netwalker.go b/pkg/idutil/netwalker/netwalker.go
deleted file mode 100644
index cd92d4eaa89..00000000000
--- a/pkg/idutil/netwalker/netwalker.go
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-package netwalker
-
-import (
- "context"
- "fmt"
- "regexp"
- "strings"
-
- "github.com/containerd/nerdctl/pkg/netutil"
-)
-
-type Found struct {
- Network *netutil.NetworkConfig
- Req string // The raw request string. name, short ID, or long ID.
- MatchIndex int // Begins with 0, up to MatchCount - 1.
- MatchCount int // 1 on exact match. > 1 on ambiguous match. Never be <= 0.
-}
-
-type OnFound func(ctx context.Context, found Found) error
-
-type NetworkWalker struct {
- Client *netutil.CNIEnv
- OnFound OnFound
-}
-
-// Walk walks networks and calls w.OnFound .
-// Req is name, short ID, or long ID.
-// Returns the number of the found entries.
-func (w *NetworkWalker) Walk(ctx context.Context, req string) (int, error) {
- longIDExp, err := regexp.Compile(fmt.Sprintf("^sha256:%s.*", regexp.QuoteMeta(req)))
- if err != nil {
- return 0, err
- }
-
- shortIDExp, err := regexp.Compile(fmt.Sprintf("^%s", regexp.QuoteMeta(req)))
- if err != nil {
- return 0, err
- }
-
- idFilterF := func(n *netutil.NetworkConfig) bool {
- if n.NerdctlID == nil {
- // External network
- return n.Name == req
- }
- return n.Name == req || longIDExp.Match([]byte(*n.NerdctlID)) || shortIDExp.Match([]byte(*n.NerdctlID))
- }
- networks, err := w.Client.FilterNetworks(idFilterF)
- if err != nil {
- return 0, err
- }
-
- matchCount := len(networks)
-
- for i, network := range networks {
- f := Found{
- Network: network,
- Req: req,
- MatchIndex: i,
- MatchCount: matchCount,
- }
- if e := w.OnFound(ctx, f); e != nil {
- return -1, e
- }
- }
- return matchCount, nil
-}
-
-// WalkAll calls `Walk` for each req in `reqs`.
-//
-// It can be used when the matchCount is not important (e.g., only care if there
-// is any error or if matchCount == 0 (not found error) when walking all reqs).
-// If `forceAll`, it calls `Walk` on every req
-// and return all errors joined by `\n`. If not `forceAll`, it returns the first error
-// encountered while calling `Walk`.
-// `allowSeudoNetwork` allows seudo network (host, none) to be passed to `Walk`, otherwise
-// an error is recorded for it.
-func (w *NetworkWalker) WalkAll(ctx context.Context, reqs []string, forceAll, allowSeudoNetwork bool) error {
- var errs []string
- for _, req := range reqs {
- if !allowSeudoNetwork && (req == "host" || req == "none") {
- err := fmt.Errorf("pseudo network not allowed: %s", req)
- if !forceAll {
- return err
- }
- errs = append(errs, err.Error())
- } else {
- n, err := w.Walk(ctx, req)
- if err == nil && n == 0 {
- err = fmt.Errorf("no such network: %s", req)
- }
- if err != nil {
- if !forceAll {
- return err
- }
- errs = append(errs, err.Error())
- }
- }
- }
- if len(errs) > 0 {
- return fmt.Errorf("%d errors:\n%s", len(errs), strings.Join(errs, "\n"))
- }
- return nil
-}
diff --git a/pkg/imageinspector/imageinspector.go b/pkg/imageinspector/imageinspector.go
index 6d98826fb77..ce1f0003f05 100644
--- a/pkg/imageinspector/imageinspector.go
+++ b/pkg/imageinspector/imageinspector.go
@@ -19,22 +19,24 @@ package imageinspector
import (
"context"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- imgutil "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
)
// Inspect inspects the image, for the platform specified in image.platform.
-func Inspect(ctx context.Context, client *containerd.Client, image images.Image, snapshotter string) (*native.Image, error) {
+func Inspect(ctx context.Context, client *containerd.Client, image images.Image, snapshotter snapshots.Snapshotter) (*native.Image, error) {
n := &native.Image{}
img := containerd.NewImage(client, image)
idx, idxDesc, err := imgutil.ReadIndex(ctx, img)
if err != nil {
- logrus.WithError(err).WithField("id", image.Name).Warnf("failed to inspect index")
+ log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to inspect index")
} else {
n.IndexDesc = idxDesc
n.Index = idx
@@ -42,7 +44,7 @@ func Inspect(ctx context.Context, client *containerd.Client, image images.Image,
mani, maniDesc, err := imgutil.ReadManifest(ctx, img)
if err != nil {
- logrus.WithError(err).WithField("id", image.Name).Warnf("failed to inspect manifest")
+ log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to inspect manifest")
} else {
n.ManifestDesc = maniDesc
n.Manifest = mani
@@ -50,15 +52,14 @@ func Inspect(ctx context.Context, client *containerd.Client, image images.Image,
imageConfig, imageConfigDesc, err := imgutil.ReadImageConfig(ctx, img)
if err != nil {
- logrus.WithError(err).WithField("id", image.Name).Warnf("failed to inspect image config")
+ log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to inspect image config")
} else {
n.ImageConfigDesc = imageConfigDesc
n.ImageConfig = imageConfig
}
- snapSvc := client.SnapshotService(snapshotter)
- n.Size, err = imgutil.UnpackedImageSize(ctx, snapSvc, img)
+ n.Size, err = imgutil.UnpackedImageSize(ctx, snapshotter, img)
if err != nil {
- logrus.WithError(err).WithField("id", image.Name).Warnf("failed to inspect calculate size")
+ log.G(ctx).WithError(err).WithField("id", image.Name).Warnf("failed to inspect calculate size")
}
n.Image = image
diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go
index a45fb939621..101cf7987f5 100644
--- a/pkg/imgutil/commit/commit.go
+++ b/pkg/imgutil/commit/commit.go
@@ -27,24 +27,29 @@ import (
"strings"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/cio"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/diff"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/leases"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/containerd/rootfs"
- "github.com/containerd/containerd/snapshots"
- imgutil "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/labels"
"github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/identity"
"github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/diff"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/leases"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/containerd/v2/pkg/cio"
+ "github.com/containerd/containerd/v2/pkg/rootfs"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/cmd/image"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ imgutil "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
)
type Changes struct {
@@ -64,7 +69,34 @@ var (
emptyDigest = digest.Digest("")
)
-func Commit(ctx context.Context, client *containerd.Client, container containerd.Container, opts *Opts) (digest.Digest, error) {
+func Commit(ctx context.Context, client *containerd.Client, container containerd.Container, opts *Opts, globalOptions types.GlobalCommandOptions) (digest.Digest, error) {
+ // Get labels
+ containerLabels, err := container.Labels(ctx)
+ if err != nil {
+ return emptyDigest, err
+ }
+
+ // Get datastore
+ dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address)
+ if err != nil {
+ return emptyDigest, err
+ }
+
+ // Ensure we do have a stateDir label
+ stateDir := containerLabels[labels.StateDir]
+ if stateDir == "" {
+ stateDir, err = containerutil.ContainerStateDirPath(globalOptions.Namespace, dataStore, container.ID())
+ if err != nil {
+ return emptyDigest, err
+ }
+ }
+
+ lf, err := containerutil.Lock(stateDir)
+ if err != nil {
+ return emptyDigest, err
+ }
+ defer lf.Release()
+
id := container.ID()
info, err := container.Info(ctx)
if err != nil {
@@ -80,13 +112,13 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd
platformLabel := info.Labels[labels.Platform]
if platformLabel == "" {
platformLabel = platforms.DefaultString()
- logrus.Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel)
+ log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel)
}
ocispecPlatform, err := platforms.Parse(platformLabel)
if err != nil {
return emptyDigest, err
}
- logrus.Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform))
+ log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform))
platformMC := platforms.Only(ocispecPlatform)
baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC)
@@ -95,12 +127,19 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd
return emptyDigest, err
}
- task, err := container.Task(ctx, cio.Load)
+ // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425
+ err = image.EnsureAllContent(ctx, client, baseImg.Name(), globalOptions)
if err != nil {
- return emptyDigest, err
+ log.G(ctx).Warn("Unable to fetch missing layers before committing. " +
+ "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.")
}
if opts.Pause {
+ task, err := container.Task(ctx, cio.Load)
+ if err != nil {
+ return emptyDigest, err
+ }
+
status, err := task.Status(ctx)
if err != nil {
return emptyDigest, err
@@ -115,7 +154,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd
defer func() {
if err := task.Resume(ctx); err != nil {
- logrus.Warnf("failed to unpause container %v: %v", id, err)
+ log.G(ctx).Warnf("failed to unpause container %v: %v", id, err)
}
}()
}
@@ -170,6 +209,13 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd
return emptyDigest, fmt.Errorf("failed to create new image %s: %w", opts.Ref, err)
}
}
+
+ // unpack the image to snapshotter
+ cimg := containerd.NewImage(client, img)
+ if err := cimg.Unpack(ctx, snName); err != nil {
+ return emptyDigest, err
+ }
+
return configDigest, nil
}
@@ -205,14 +251,14 @@ func generateCommitImageConfig(ctx context.Context, container containerd.Contain
arch := baseConfig.Architecture
if arch == "" {
arch = runtime.GOARCH
- logrus.Warnf("assuming arch=%q", arch)
+ log.G(ctx).Warnf("assuming arch=%q", arch)
}
os := baseConfig.OS
if os == "" {
os = runtime.GOOS
- logrus.Warnf("assuming os=%q", os)
+ log.G(ctx).Warnf("assuming os=%q", os)
}
- logrus.Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os)
+ log.G(ctx).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os)
return ocispec.Image{
Platform: ocispec.Platform{
Architecture: arch,
@@ -352,7 +398,7 @@ func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn
// NOTE: the snapshotter should be hold by lease. Even
// if the cleanup fails, the containerd gc can delete it.
if err := sn.Remove(ctx, key); err != nil {
- logrus.Warnf("failed to cleanup aborted apply %s: %s", key, err)
+ log.G(ctx).Warnf("failed to cleanup aborted apply %s: %s", key, err)
}
}
}()
diff --git a/pkg/imgutil/converter/zstd.go b/pkg/imgutil/converter/zstd.go
new file mode 100644
index 00000000000..bb177a63442
--- /dev/null
+++ b/pkg/imgutil/converter/zstd.go
@@ -0,0 +1,123 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package converter
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/klauspost/compress/zstd"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/converter"
+ "github.com/containerd/containerd/v2/core/images/converter/uncompress"
+ "github.com/containerd/containerd/v2/pkg/archive/compression"
+ "github.com/containerd/errdefs"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+)
+
+// ZstdLayerConvertFunc converts legacy tar.gz layers into zstd layers with
+// the specified compression level.
+func ZstdLayerConvertFunc(options types.ImageConvertOptions) (converter.ConvertFunc, error) {
+ return func(ctx context.Context, cs content.Store, desc ocispec.Descriptor) (*ocispec.Descriptor, error) {
+ if !images.IsLayerType(desc.MediaType) {
+ // No conversion. No need to return an error here.
+ return nil, nil
+ }
+ var err error
+ // Read it
+ readerAt, err := cs.ReaderAt(ctx, desc)
+ if err != nil {
+ return nil, err
+ }
+ defer readerAt.Close()
+ sectionReader := io.NewSectionReader(readerAt, 0, desc.Size)
+
+ info, err := cs.Info(ctx, desc.Digest)
+ if err != nil {
+ return nil, err
+ }
+
+ var oldReader io.Reader
+ // If it is compressed, get a decompressed stream
+ if !uncompress.IsUncompressedType(desc.MediaType) {
+ decompStream, err := compression.DecompressStream(sectionReader)
+ if err != nil {
+ return nil, err
+ }
+ defer decompStream.Close()
+ oldReader = decompStream
+ } else {
+ oldReader = sectionReader
+ }
+
+ ref := fmt.Sprintf("convert-zstd-from-%s", desc.Digest)
+ w, err := content.OpenWriter(ctx, cs, content.WithRef(ref))
+ if err != nil {
+ return nil, err
+ }
+ defer w.Close()
+
+ // Reset the writing position
+ // Old writer possibly remains without aborted
+ // (e.g. conversion interrupted by a signal)
+ if err := w.Truncate(0); err != nil {
+ return nil, err
+ }
+
+ pr, pw := io.Pipe()
+ enc, err := zstd.NewWriter(pw, zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(options.ZstdCompressionLevel)))
+ if err != nil {
+ return nil, err
+ }
+ go func() {
+ if _, err := io.Copy(enc, oldReader); err != nil {
+ pr.CloseWithError(err)
+ return
+ }
+ if err = enc.Close(); err != nil {
+ pr.CloseWithError(err)
+ return
+ }
+ if err = pw.Close(); err != nil {
+ pr.CloseWithError(err)
+ return
+ }
+ }()
+
+ n, err := io.Copy(w, pr)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = w.Commit(ctx, 0, "", content.WithLabels(info.Labels)); err != nil && !errdefs.IsAlreadyExists(err) {
+ return nil, err
+ }
+ if err := w.Close(); err != nil {
+ return nil, err
+ }
+ newDesc := desc
+ newDesc.Digest = w.Digest()
+ newDesc.Size = n
+ newDesc.MediaType = ocispec.MediaTypeImageLayerZstd
+ return &newDesc, nil
+ }, nil
+}
diff --git a/pkg/imgutil/dockerconfigresolver/credentialsstore.go b/pkg/imgutil/dockerconfigresolver/credentialsstore.go
new file mode 100644
index 00000000000..b08deba271c
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/credentialsstore.go
@@ -0,0 +1,174 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/docker/cli/cli/config"
+ "github.com/docker/cli/cli/config/configfile"
+ "github.com/docker/cli/cli/config/types"
+)
+
+type Credentials = types.AuthConfig
+
+// NewCredentialsStore returns a CredentialsStore from a directory
+// If path is left empty, the default docker `~/.docker/config.json` will be used
+// In case the docker call fails, we wrap the error with ErrUnableToInstantiate
+func NewCredentialsStore(path string) (*CredentialsStore, error) {
+ dockerConfigFile, err := config.Load(path)
+ if err != nil {
+ return nil, errors.Join(ErrUnableToInstantiate, err)
+ }
+
+ return &CredentialsStore{
+ dockerConfigFile: dockerConfigFile,
+ }, nil
+}
+
+// CredentialsStore is an abstraction in front of docker config API manipulation
+// exposing just the limited functions we need and hiding away url normalization / identifiers magic, and handling of
+// backward compatibility
+type CredentialsStore struct {
+ dockerConfigFile *configfile.ConfigFile
+}
+
+// Erase will remove any and all stored credentials for that registry namespace (including all legacy variants)
+// If we do not find at least ONE variant matching the namespace, this will error with ErrUnableToErase
+func (cs *CredentialsStore) Erase(registryURL *RegistryURL) (map[string]error, error) {
+ // Get all associated identifiers for that registry including legacy ones and variants
+ logoutList := registryURL.AllIdentifiers()
+
+ // Iterate through and delete them one by one
+ errs := make(map[string]error)
+ for _, serverAddress := range logoutList {
+ if err := cs.dockerConfigFile.GetCredentialsStore(serverAddress).Erase(serverAddress); err != nil {
+ errs[serverAddress] = err
+ }
+ }
+
+ // If we succeeded removing at least one, it is a success.
+ // The only error condition is if we failed removing anything - meaning there was no such credential information
+ // in whatever format - or the store is broken.
+ if len(errs) == len(logoutList) {
+ return errs, ErrUnableToErase
+ }
+
+ return nil, nil
+}
+
+// Store will save credentials for a given registry
+// On error, ErrUnableToStore
+func (cs *CredentialsStore) Store(registryURL *RegistryURL, credentials *Credentials) error {
+ // We just overwrite the server property here with the host
+ // Whether it was one of the variants, or was not set at all (see for example Amazon ECR, https://github.com/containerd/nerdctl/issues/733
+ // - which is likely a bug in docker) it doesn't matter.
+ // This is the credentials that were returned for that host, by the docker credentials store.
+ if registryURL.Namespace != nil {
+ credentials.ServerAddress = fmt.Sprintf("%s%s?%s", registryURL.Host, registryURL.Path, registryURL.RawQuery)
+ } else {
+ credentials.ServerAddress = registryURL.CanonicalIdentifier()
+ }
+
+ // XXX future namespaced url likely require special handling here
+ if err := cs.dockerConfigFile.GetCredentialsStore(registryURL.CanonicalIdentifier()).Store(*(credentials)); err != nil {
+ return errors.Join(ErrUnableToStore, err)
+ }
+
+ return nil
+}
+
+// ShellCompletion will return candidate strings for nerdctl logout
+func (cs *CredentialsStore) ShellCompletion() []string {
+ candidates := []string{}
+ for key := range cs.dockerConfigFile.AuthConfigs {
+ candidates = append(candidates, key)
+ }
+
+ return candidates
+}
+
+// FileStorageLocation will return the file where credentials are stored for a given registry, or the empty string
+// if it is stored / to be stored in a different place (like an OS keychain, with docker credential helpers)
+func (cs *CredentialsStore) FileStorageLocation(registryURL *RegistryURL) string {
+ if store, isFile := (cs.dockerConfigFile.GetCredentialsStore(registryURL.CanonicalIdentifier())).(isFileStore); isFile {
+ return store.GetFilename()
+ }
+
+ return ""
+}
+
+// Retrieve gets existing credentials from the store for a certain registry.
+// If none are found, an empty Credentials struct is returned.
+// If we hard-fail reading from the store, indicative of a broken system, we wrap the error with ErrUnableToRetrieve
+func (cs *CredentialsStore) Retrieve(registryURL *RegistryURL, checkCredStore bool) (*Credentials, error) {
+ var err error
+ returnedCredentials := &Credentials{}
+
+ // As long as we depend on .ServerAddress, make sure it is populated correctly
+ // It does not matter what was stored - the docker cli clearly has issues with this
+ // What matters is that the credentials retrieved from the docker credentials store are *for that registryURL*
+ // and that is what ServerAddress should point to
+ defer func() {
+ if registryURL.Namespace != nil {
+ returnedCredentials.ServerAddress = fmt.Sprintf("%s%s?%s", registryURL.Host, registryURL.Path, registryURL.RawQuery)
+ } else {
+ returnedCredentials.ServerAddress = registryURL.Host
+ }
+ }()
+
+ if !checkCredStore {
+ return returnedCredentials, nil
+ }
+
+ // Get the legacy variants (w/o scheme or port), and iterate over until we find one with credentials
+ variants := registryURL.AllIdentifiers()
+
+ for _, identifier := range variants {
+ var credentials types.AuthConfig
+ // Note that Get does not raise an error on ENOENT
+ credentials, err = cs.dockerConfigFile.GetCredentialsStore(identifier).Get(identifier)
+ if err != nil {
+ continue
+ }
+ returnedCredentials = &credentials
+ // Clean-up the username
+ returnedCredentials.Username = strings.TrimSpace(returnedCredentials.Username)
+ // Stop here if we found credentials with this variant
+ if returnedCredentials.IdentityToken != "" ||
+ returnedCredentials.Username != "" ||
+ returnedCredentials.Password != "" ||
+ returnedCredentials.RegistryToken != "" {
+ break
+ }
+ }
+
+ // (Last non nil) credential store error gets wrapped into ErrUnableToRetrieve
+ if err != nil {
+ err = errors.Join(ErrUnableToRetrieve, err)
+ }
+
+ return returnedCredentials, err
+}
+
+// isFileStore is an internal mock interface purely meant to help identify that the docker credential backend is a filesystem one
+type isFileStore interface {
+ IsFileStore() bool
+ GetFilename() string
+}
diff --git a/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go b/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go
new file mode 100644
index 00000000000..003fe1aa211
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/credentialsstore_test.go
@@ -0,0 +1,401 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import (
+ "encoding/base64"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func createTempDir(t *testing.T, mode os.FileMode) string {
+ tmpDir, err := os.MkdirTemp(t.TempDir(), "docker-config")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Chmod(tmpDir, mode)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+}
+
+func TestBrokenCredentialsStore(t *testing.T) {
+ if runtime.GOOS == "freebsd" {
+ // It is unclear why these tests are failing on FreeBSD, and if it is a problem with Vagrant or differences
+ // with FreeBSD
+ // Anyhow, this test is about extreme cases & conditions (filesystem errors wrt credentials loading).
+ t.Skip("skipping broken credential store tests for freebsd")
+ }
+ if runtime.GOOS == "windows" {
+ // Same as above
+ t.Skip("test is not compatible with windows")
+ }
+
+ testCases := []struct {
+ description string
+ setup func() string
+ errorNew error
+ errorRead error
+ errorWrite error
+ }{
+ {
+ description: "Pointing DOCKER_CONFIG at a non-existent directory inside an unreadable directory will prevent instantiation",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0000)
+ return filepath.Join(tmpDir, "doesnotexistcantcreate")
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a non-existent directory inside a read-only directory will prevent saving credentials",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0500)
+ return filepath.Join(tmpDir, "doesnotexistcantcreate")
+ },
+ errorWrite: ErrUnableToStore,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at an unreadable directory will prevent instantiation",
+ setup: func() string {
+ return createTempDir(t, 0000)
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a read-only directory will prevent saving credentials",
+ setup: func() string {
+ return createTempDir(t, 0500)
+ },
+ errorWrite: ErrUnableToStore,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a directory containing am unparsable `config.json` will prevent instantiation",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("porked"), 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a file instead of a directory will prevent instantiation",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ fd, err := os.OpenFile(filepath.Join(tmpDir, "isafile"), os.O_CREATE, 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = fd.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return filepath.Join(tmpDir, "isafile")
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a directory containing a `config.json` directory will prevent instantiation",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ err := os.Mkdir(filepath.Join(tmpDir, "config.json"), 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a directory containing a `config.json` dangling symlink will still work",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ err := os.Symlink("doesnotexist", filepath.Join(tmpDir, "config.json"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+ },
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a directory containing an unreadable, valid `config.json` file will prevent instantiation",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Chmod(filepath.Join(tmpDir, "config.json"), 0000)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+ },
+ errorNew: ErrUnableToInstantiate,
+ },
+ {
+ description: "Pointing DOCKER_CONFIG at a directory containing a read-only, valid `config.json` file will NOT prevent saving credentials",
+ setup: func() string {
+ tmpDir := createTempDir(t, 0700)
+ err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte("{}"), 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = os.Chmod(filepath.Join(tmpDir, "config.json"), 0400)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+ },
+ },
+ }
+
+ t.Run("Docker Config testing with a variety of filesystem situations", func(t *testing.T) {
+ // Do NOT parallelize this test, as it relies on Chdir, which would have side effects for other tests.
+ registryURL, err := Parse("registry")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ for _, testCase := range testCases {
+ tc := testCase
+ t.Run(tc.description, func(tt *testing.T) {
+ // See https://github.com/containerd/nerdctl/issues/3413
+ var oldpwd string
+ directory := tc.setup()
+ oldpwd, err = os.Getwd()
+ assert.NilError(tt, err)
+ // Ignore the error, as the destination may not be a directory
+ _ = os.Chdir(directory)
+ tt.Cleanup(func() {
+ err = os.Chdir(oldpwd)
+ assert.NilError(tt, err)
+ })
+
+ var cs *CredentialsStore
+ cs, err = NewCredentialsStore(directory)
+ assert.ErrorIs(tt, err, tc.errorNew)
+ if err != nil {
+ return
+ }
+
+ var af *Credentials
+ af, err = cs.Retrieve(registryURL, true)
+ assert.ErrorIs(tt, err, tc.errorRead)
+
+ err = cs.Store(registryURL, af)
+ assert.ErrorIs(tt, err, tc.errorWrite)
+ })
+ }
+ })
+}
+
+func writeContent(t *testing.T, content string) string {
+ t.Helper()
+ tmpDir := createTempDir(t, 0700)
+ err := os.WriteFile(filepath.Join(tmpDir, "config.json"), []byte(content), 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return tmpDir
+}
+
+func TestWorkingCredentialsStore(t *testing.T) {
+ testCases := []struct {
+ description string
+ setup func() string
+ username string
+ password string
+ }{
+ {
+ description: "Reading credentials from `auth` using canonical identifier",
+ username: "username",
+ password: "password",
+ setup: func() string {
+ content := fmt.Sprintf(`{
+ "auths": {
+ "registry.example:443": {
+ "auth": %q
+ }
+ }
+ }`, base64.StdEncoding.EncodeToString([]byte("username:password")))
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Reading from legacy / alternative identifiers: registry.example",
+ username: "username",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "registry.example": {
+ "username": "username"
+ }
+ }
+ }`
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Reading from legacy / alternative identifiers: http://registry.example",
+ username: "username",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "http://registry.example": {
+ "username": "username"
+ }
+ }
+ }`
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Reading from legacy / alternative identifiers: https://registry.example",
+ username: "username",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "https://registry.example": {
+ "username": "username"
+ }
+ }
+ }`
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Reading from legacy / alternative identifiers: http://registry.example:443",
+ username: "username",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "http://registry.example:443": {
+ "username": "username"
+ }
+ }
+ }`
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Reading from legacy / alternative identifiers: https://registry.example:443",
+ username: "username",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "https://registry.example:443": {
+ "username": "username"
+ }
+ }
+ }`
+ return writeContent(t, content)
+ },
+ },
+ {
+ description: "Canonical form is preferred over legacy forms",
+ username: "pick",
+ setup: func() string {
+ content := `{
+ "auths": {
+ "http://registry.example:443": {
+ "username": "ignore"
+ },
+ "https://registry.example:443": {
+ "username": "ignore"
+ },
+ "registry.example": {
+ "username": "ignore"
+ },
+ "registry.example:443": {
+ "serveraddress": "bla",
+ "username": "pick"
+ },
+ "http://registry.example": {
+ "username": "ignore"
+ },
+ "https://registry.example": {
+ "username": "ignore"
+ }
+ }
+}`
+ return writeContent(t, content)
+ },
+ },
+ }
+
+ t.Run("Working credentials store", func(t *testing.T) {
+
+ for _, tc := range testCases {
+ t.Run(tc.description, func(t *testing.T) {
+ registryURL, err := Parse("registry.example")
+ if err != nil {
+ t.Fatal(err)
+ }
+ cs, err := NewCredentialsStore(tc.setup())
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var af *Credentials
+ af, err = cs.Retrieve(registryURL, true)
+ assert.ErrorIs(t, err, nil)
+ assert.Equal(t, af.Username, tc.username)
+ assert.Equal(t, af.ServerAddress, "registry.example:443")
+ assert.Equal(t, af.Password, tc.password)
+ })
+ }
+ })
+
+ t.Run("Namespaced host", func(t *testing.T) {
+ server := "host.example/path?ns=namespace.example"
+ registryURL, err := Parse(server)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ content := `{
+ "auths": {
+ "nerdctl-experimental://namespace.example:443/host/host.example:443/path": {
+ "username": "username"
+ }
+ }
+ }`
+ dir := writeContent(t, content)
+ cs, err := NewCredentialsStore(dir)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var af *Credentials
+ af, err = cs.Retrieve(registryURL, true)
+ assert.ErrorIs(t, err, nil)
+ assert.Equal(t, af.Username, "username")
+ assert.Equal(t, af.ServerAddress, "host.example:443/path?ns=namespace.example")
+
+ })
+}
+
+// TODO: add more tests that write credentials (specifically to hub locations) to verify they use the canonical id properly
diff --git a/pkg/imgutil/dockerconfigresolver/defaults.go b/pkg/imgutil/dockerconfigresolver/defaults.go
new file mode 100644
index 00000000000..ea91279e3a9
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/defaults.go
@@ -0,0 +1,52 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import "errors"
+
+type scheme string
+
+const (
+ StandardHTTPSPort = "443"
+
+ schemeHTTPS scheme = "https"
+ schemeHTTP scheme = "http"
+ // schemeNerdctlExperimental is currently provisional, to unlock namespace based host authentication
+ // This may change or break without notice, and you should have no expectations that credentials saved like that
+ // will be supported in the future
+ schemeNerdctlExperimental scheme = "nerdctl-experimental"
+ // See https://github.com/moby/moby/blob/v27.1.1/registry/config.go#L42-L48
+ //nolint:misspell
+ // especially Sebastiaan comments on future domain consolidation
+ dockerIndexServer = "https://index.docker.io/v1/"
+ // The query parameter that containerd will slap on namespaced hosts
+ namespaceQueryParameter = "ns"
+)
+
+// Errors returned by the credentials store
+var (
+ ErrUnableToInstantiate = errors.New("unable to instantiate docker credentials store")
+ ErrUnableToErase = errors.New("unable to erase credentials")
+ ErrUnableToStore = errors.New("unable to store credentials")
+ ErrUnableToRetrieve = errors.New("unable to retrieve credentials")
+)
+
+// Errors returned by `Parse`
+var (
+ ErrUnparsableURL = errors.New("unparsable registry URL")
+ ErrUnsupportedScheme = errors.New("unsupported scheme in registry URL")
+)
diff --git a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go
index db9bd7b265f..8577b8e2bc6 100644
--- a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go
+++ b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver.go
@@ -20,17 +20,12 @@ import (
"context"
"crypto/tls"
"errors"
- "fmt"
- "os"
-
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/containerd/remotes/docker"
- dockerconfig "github.com/containerd/containerd/remotes/docker/config"
- dockercliconfig "github.com/docker/cli/cli/config"
- "github.com/docker/cli/cli/config/credentials"
- dockercliconfigtypes "github.com/docker/cli/cli/config/types"
- "github.com/docker/docker/errdefs"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/containerd/v2/core/remotes/docker"
+ dockerconfig "github.com/containerd/containerd/v2/core/remotes/docker/config"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
)
var PushTracker = docker.NewInMemoryTracker()
@@ -61,24 +56,9 @@ func WithSkipVerifyCerts(b bool) Opt {
// WithHostsDirs specifies directories like /etc/containerd/certs.d and /etc/docker/certs.d
func WithHostsDirs(orig []string) Opt {
- var ss []string
- if len(orig) == 0 {
- logrus.Debug("no hosts dir was specified")
- }
- for _, v := range orig {
- if _, err := os.Stat(v); err == nil {
- logrus.Debugf("Found hosts dir %q", v)
- ss = append(ss, v)
- } else {
- if errors.Is(err, os.ErrNotExist) {
- logrus.WithError(err).Debugf("Ignoring hosts dir %q", v)
- } else {
- logrus.WithError(err).Warnf("Ignoring hosts dir %q", v)
- }
- }
- }
+ validDirs := validateDirectories(orig)
return func(o *opts) {
- o.hostsDirs = ss
+ o.hostsDirs = validDirs
}
}
@@ -100,14 +80,26 @@ func NewHostOptions(ctx context.Context, refHostname string, optFuncs ...Opt) (*
}
var ho dockerconfig.HostOptions
- ho.HostDir = func(s string) (string, error) {
- for _, hostsDir := range o.hostsDirs {
- found, err := dockerconfig.HostDirFromRoot(hostsDir)(s)
- if (err != nil && !errdefs.IsNotFound(err)) || (found != "") {
- return found, err
+ ho.HostDir = func(hostURL string) (string, error) {
+ regURL, err := Parse(hostURL)
+ // Docker inconsistencies handling: `index.docker.io` actually expects `docker.io` for hosts.toml on the filesystem
+ // See https://github.com/containerd/nerdctl/issues/3697
+ // FIXME: we need to reevaluate this comparing with what docker does. What should happen for FQ images with alternate docker domains? (eg: registry-1.docker.io)
+ if regURL.Hostname() == "index.docker.io" {
+ regURL.Host = "docker.io"
+ }
+
+ if err != nil {
+ return "", err
+ }
+ dir, err := hostDirsFromRoot(regURL, o.hostsDirs)
+ if err != nil {
+ if errors.Is(err, errdefs.ErrNotFound) {
+ err = nil
}
+ return "", err
}
- return "", nil
+ return dir, nil
}
if o.authCreds != nil {
@@ -136,6 +128,10 @@ func NewHostOptions(ctx context.Context, refHostname string, optFuncs ...Opt) (*
ho.DefaultScheme = "http"
}
}
+ if ho.DefaultScheme == "http" {
+ // https://github.com/containerd/containerd/issues/9208
+ ho.DefaultTLS = nil
+ }
return &ho, nil
}
@@ -165,85 +161,35 @@ type AuthCreds func(string) (string, string, error)
// NewAuthCreds returns AuthCreds that uses $DOCKER_CONFIG/config.json .
// AuthCreds can be nil.
func NewAuthCreds(refHostname string) (AuthCreds, error) {
- // Load does not raise an error on ENOENT
- dockerConfigFile, err := dockercliconfig.Load("")
+ // Note: does not raise an error on ENOENT
+ credStore, err := NewCredentialsStore("")
if err != nil {
return nil, err
}
- // DefaultHost converts "docker.io" to "registry-1.docker.io",
- // which is wanted by credFunc .
- credFuncExpectedHostname, err := docker.DefaultHost(refHostname)
- if err != nil {
- return nil, err
- }
+ credFunc := func(host string) (string, string, error) {
+ rHost, err := Parse(host)
+ if err != nil {
+ return "", "", err
+ }
- var credFunc AuthCreds
+ ac, err := credStore.Retrieve(rHost, true)
+ if err != nil {
+ return "", "", err
+ }
- authConfigHostnames := []string{refHostname}
- if refHostname == "docker.io" || refHostname == "registry-1.docker.io" {
- // "docker.io" appears as ""https://index.docker.io/v1/" in ~/.docker/config.json .
- // Unlike other registries, we have to pass the full URL to GetAuthConfig.
- authConfigHostnames = append([]string{IndexServer}, refHostname)
- }
+ if ac.IdentityToken != "" {
+ return "", ac.IdentityToken, nil
+ }
- for _, authConfigHostname := range authConfigHostnames {
- // GetAuthConfig does not raise an error on ENOENT
- ac, err := dockerConfigFile.GetAuthConfig(authConfigHostname)
- if err != nil {
- logrus.WithError(err).Warnf("cannot get auth config for authConfigHostname=%q (refHostname=%q)",
- authConfigHostname, refHostname)
- } else {
- // When refHostname is "docker.io":
- // - credFuncExpectedHostname: "registry-1.docker.io"
- // - credFuncArg: "registry-1.docker.io"
- // - authConfigHostname: "https://index.docker.io/v1/" (IndexServer)
- // - ac.ServerAddress: "https://index.docker.io/v1/".
- if !isAuthConfigEmpty(ac) {
- if ac.ServerAddress == "" {
- // This can happen with Amazon ECR: https://github.com/containerd/nerdctl/issues/733
- logrus.Debugf("failed to get ac.ServerAddress for authConfigHostname=%q (refHostname=%q)",
- authConfigHostname, refHostname)
- } else if authConfigHostname == IndexServer {
- if ac.ServerAddress != IndexServer {
- return nil, fmt.Errorf("expected ac.ServerAddress (%q) to be %q", ac.ServerAddress, IndexServer)
- }
- } else {
- acsaHostname := credentials.ConvertToHostname(ac.ServerAddress)
- if acsaHostname != authConfigHostname {
- return nil, fmt.Errorf("expected the hostname part of ac.ServerAddress (%q) to be authConfigHostname=%q, got %q",
- ac.ServerAddress, authConfigHostname, acsaHostname)
- }
- }
-
- if ac.RegistryToken != "" {
- // Even containerd/CRI does not support RegistryToken as of v1.4.3,
- // so, nobody is actually using RegistryToken?
- logrus.Warnf("ac.RegistryToken (for %q) is not supported yet (FIXME)", authConfigHostname)
- }
-
- credFunc = func(credFuncArg string) (string, string, error) {
- // credFuncArg should be like "registry-1.docker.io"
- if credFuncArg != credFuncExpectedHostname {
- return "", "", fmt.Errorf("expected credFuncExpectedHostname=%q (refHostname=%q), got credFuncArg=%q",
- credFuncExpectedHostname, refHostname, credFuncArg)
- }
- if ac.IdentityToken != "" {
- return "", ac.IdentityToken, nil
- }
- return ac.Username, ac.Password, nil
- }
- break
- }
+ if ac.RegistryToken != "" {
+ // Even containerd/CRI does not support RegistryToken as of v1.4.3,
+ // so, nobody is actually using RegistryToken?
+ log.L.Warnf("ac.RegistryToken (for %q) is not supported yet (FIXME)", rHost.Host)
}
- }
- // credsFunc can be nil here
- return credFunc, nil
-}
-func isAuthConfigEmpty(ac dockercliconfigtypes.AuthConfig) bool {
- if ac.IdentityToken != "" || ac.Username != "" || ac.Password != "" || ac.RegistryToken != "" {
- return false
+ return ac.Username, ac.Password, nil
}
- return true
+
+ return credFunc, nil
}
diff --git a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver_util.go b/pkg/imgutil/dockerconfigresolver/dockerconfigresolver_util.go
deleted file mode 100644
index b452425e7ba..00000000000
--- a/pkg/imgutil/dockerconfigresolver/dockerconfigresolver_util.go
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- Copyright The containerd Authors.
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-*/
-
-/*
- Portions from https://github.com/moby/moby/blob/v20.10.18/registry/auth.go#L154-L167
- Copyright (C) Docker/Moby authors.
- Licensed under the Apache License, Version 2.0
- NOTICE: https://github.com/moby/moby/blob/v20.10.18/NOTICE
-*/
-
-package dockerconfigresolver
-
-import (
- "strings"
-)
-
-// IndexServer is used for user auth and image search
-//
-// From https://github.com/moby/moby/blob/v20.10.18/registry/config.go#L36-L39
-const IndexServer = "https://index.docker.io/v1/"
-
-// ConvertToHostname converts a registry url which has http|https prepended
-// to just an hostname.
-//
-// From https://github.com/moby/moby/blob/v20.10.18/registry/auth.go#L154-L167
-func ConvertToHostname(url string) string {
- stripped := url
- if strings.HasPrefix(url, "http://") {
- stripped = strings.TrimPrefix(url, "http://")
- } else if strings.HasPrefix(url, "https://") {
- stripped = strings.TrimPrefix(url, "https://")
- }
-
- nameParts := strings.SplitN(stripped, "/", 2)
-
- return nameParts[0]
-}
diff --git a/pkg/imgutil/dockerconfigresolver/hostsstore.go b/pkg/imgutil/dockerconfigresolver/hostsstore.go
new file mode 100644
index 00000000000..12df6961455
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/hostsstore.go
@@ -0,0 +1,66 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import (
+ "errors"
+ "os"
+
+ "github.com/containerd/containerd/v2/core/remotes/docker/config"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+)
+
+// validateDirectories inspect a slice of strings and returns the ones that are valid readable directories
+func validateDirectories(orig []string) []string {
+ ss := []string{}
+ for _, v := range orig {
+ fi, err := os.Stat(v)
+ if err != nil || !fi.IsDir() {
+ if !errors.Is(err, os.ErrNotExist) {
+ log.L.WithError(err).Warnf("Ignoring hosts location %q", v)
+ }
+ continue
+ }
+ ss = append(ss, v)
+ }
+ return ss
+}
+
+// hostDirsFromRoot will retrieve a host.toml file for the namespace host, possibly trying without port
+// if the requested port is standard.
+// https://github.com/containerd/nerdctl/issues/3047
+func hostDirsFromRoot(registryURL *RegistryURL, dirs []string) (string, error) {
+ hostsDirs := validateDirectories(dirs)
+
+ // Go through the configured system location to consider for hosts.toml files
+ for _, hostsDir := range hostsDirs {
+ found, err := config.HostDirFromRoot(hostsDir)(registryURL.Host)
+ // If we errored with anything but NotFound, or if we found one, return now
+ if (err != nil && !errdefs.IsNotFound(err)) || (found != "") {
+ return found, err
+ }
+ // If not found, and the port is standard, try again without the port
+ if registryURL.Port() == StandardHTTPSPort {
+ found, err = config.HostDirFromRoot(hostsDir)(registryURL.Hostname())
+ if (err != nil && !errors.Is(err, errdefs.ErrNotFound)) || (found != "") {
+ return found, err
+ }
+ }
+ }
+ return "", nil
+}
diff --git a/pkg/imgutil/dockerconfigresolver/registryurl.go b/pkg/imgutil/dockerconfigresolver/registryurl.go
new file mode 100644
index 00000000000..ec195e1068c
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/registryurl.go
@@ -0,0 +1,136 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "net/url"
+ "strings"
+)
+
+// Parse will return a normalized Docker Registry url from the provided string address
+func Parse(address string) (*RegistryURL, error) {
+ var err error
+ // No address or address as docker.io? Default to standardized index
+ if address == "" || address == "docker.io" {
+ address = dockerIndexServer
+ }
+ // If it has no scheme, slap one just so we can parse
+ if !strings.Contains(address, "://") {
+ address = fmt.Sprintf("%s://%s", schemeHTTPS, address)
+ }
+ // Parse it
+ u, err := url.Parse(address)
+ if err != nil {
+ return nil, errors.Join(ErrUnparsableURL, err)
+ }
+ sch := scheme(u.Scheme)
+ // Scheme is entirely disregarded anyhow, so, just drop it all and set to https
+ if sch == schemeHTTP {
+ u.Scheme = string(schemeHTTPS)
+ } else if sch != schemeHTTPS && sch != schemeNerdctlExperimental {
+ // Docker is wildly buggy when it comes to non-http schemes. Being more defensive.
+ return nil, ErrUnsupportedScheme
+ }
+ // If it has no port, add the standard port explicitly
+ if u.Port() == "" {
+ u.Host = u.Hostname() + ":" + StandardHTTPSPort
+ }
+ reg := &RegistryURL{URL: *u}
+ queryParams := u.Query()
+ nsQuery := queryParams.Get(namespaceQueryParameter)
+ if nsQuery != "" {
+ reg.Namespace, err = Parse(nsQuery)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return reg, nil
+}
+
+// RegistryURL is a struct that represents a registry namespace or host, meant specifically to deal with
+// credentials storage and retrieval inside Docker config file.
+type RegistryURL struct {
+ url.URL
+ Namespace *RegistryURL
+}
+
+// CanonicalIdentifier returns the identifier expected to be used to save credentials to docker auth config
+func (rn *RegistryURL) CanonicalIdentifier() string {
+ // If it is the docker index over https, port 443, on the /v1/ path, we use the docker fully qualified identifier
+ if rn.Scheme == string(schemeHTTPS) && rn.Hostname() == "index.docker.io" && rn.Path == "/v1/" && rn.Port() == StandardHTTPSPort ||
+ rn.URL.String() == dockerIndexServer {
+ return dockerIndexServer
+ }
+ // Otherwise, for anything else, we use the hostname+port part
+ identifier := rn.Host
+ // If this is a namespaced entry, wrap it, and slap the path as well, as hosts are allowed to be non-compliant
+ if rn.Namespace != nil {
+ identifier = fmt.Sprintf("%s://%s/host/%s%s", schemeNerdctlExperimental, rn.Namespace.CanonicalIdentifier(), identifier, rn.Path)
+ }
+ return identifier
+}
+
+// AllIdentifiers returns a list of identifiers that may have been used to save credentials,
+// accounting for legacy formats including scheme, with and without ports
+func (rn *RegistryURL) AllIdentifiers() []string {
+ canonicalID := rn.CanonicalIdentifier()
+ fullList := []string{
+ // This is rn.Host, and always have a port (see parsing)
+ canonicalID,
+ }
+ // If the canonical identifier points to Docker Hub, or is one of our experimental ids, there is no alternative / legacy id
+ if canonicalID == dockerIndexServer || rn.Namespace != nil {
+ return fullList
+ }
+
+ // Docker behavior: if the domain was index.docker.io over 443, we are allowed to additionally read the canonical
+ // docker credentials
+ if rn.Port() == StandardHTTPSPort {
+ if rn.Hostname() == "index.docker.io" || rn.Hostname() == "registry-1.docker.io" {
+ fullList = append(fullList, dockerIndexServer)
+ }
+ }
+
+ // Add legacy variants
+ fullList = append(fullList,
+ fmt.Sprintf("%s://%s", schemeHTTPS, rn.Host),
+ fmt.Sprintf("%s://%s", schemeHTTP, rn.Host),
+ )
+
+ // Note that docker does not try to be smart wrt explicit port vs. implied port
+ // If standard port, allow retrieving credentials from the variant without a port as well
+ if rn.Port() == StandardHTTPSPort {
+ fullList = append(
+ fullList,
+ rn.Hostname(),
+ fmt.Sprintf("%s://%s", schemeHTTPS, rn.Hostname()),
+ fmt.Sprintf("%s://%s", schemeHTTP, rn.Hostname()),
+ )
+ }
+
+ return fullList
+}
+
+func (rn *RegistryURL) IsLocalhost() bool {
+ // Containerd exposes both a IsLocalhost and a MatchLocalhost method
+ // There does not seem to be a clear reason for the duplication, nor the differences in implementation.
+ // Either way, they both reparse the host with net.SplitHostPort, which is unnecessary here
+ return rn.Hostname() == "localhost" || net.ParseIP(rn.Hostname()).IsLoopback()
+}
diff --git a/pkg/imgutil/dockerconfigresolver/registryurl_test.go b/pkg/imgutil/dockerconfigresolver/registryurl_test.go
new file mode 100644
index 00000000000..d10a02028e2
--- /dev/null
+++ b/pkg/imgutil/dockerconfigresolver/registryurl_test.go
@@ -0,0 +1,194 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockerconfigresolver
+
+import (
+ "testing"
+
+ "gotest.tools/v3/assert"
+)
+
+func TestURLParsingAndID(t *testing.T) {
+ tests := []struct {
+ address string
+ error error
+ identifier string
+ allIDs []string
+ isLocalhost bool
+ }{
+ {
+ address: "∞://",
+ error: ErrUnparsableURL,
+ },
+ {
+ address: "whatever://",
+ error: ErrUnsupportedScheme,
+ },
+ {
+ address: "",
+ identifier: "https://index.docker.io/v1/",
+ allIDs: []string{"https://index.docker.io/v1/"},
+ },
+ {
+ address: "https://index.docker.io/v1/",
+ identifier: "https://index.docker.io/v1/",
+ allIDs: []string{"https://index.docker.io/v1/"},
+ },
+ {
+ address: "index.docker.io",
+ identifier: "index.docker.io:443",
+ allIDs: []string{
+ "index.docker.io:443",
+ "https://index.docker.io/v1/",
+ "https://index.docker.io:443", "http://index.docker.io:443",
+ "index.docker.io", "https://index.docker.io", "http://index.docker.io",
+ },
+ },
+ {
+ address: "index.docker.io/whatever",
+ identifier: "index.docker.io:443",
+ allIDs: []string{
+ "index.docker.io:443",
+ "https://index.docker.io/v1/",
+ "https://index.docker.io:443", "http://index.docker.io:443",
+ "index.docker.io", "https://index.docker.io", "http://index.docker.io",
+ },
+ },
+ {
+ address: "http://index.docker.io",
+ identifier: "index.docker.io:443",
+ allIDs: []string{
+ "index.docker.io:443",
+ "https://index.docker.io/v1/",
+ "https://index.docker.io:443", "http://index.docker.io:443",
+ "index.docker.io", "https://index.docker.io", "http://index.docker.io",
+ },
+ },
+ {
+ address: "index.docker.io:80",
+ identifier: "index.docker.io:80",
+ allIDs: []string{
+ "index.docker.io:80",
+ "https://index.docker.io:80", "http://index.docker.io:80",
+ },
+ },
+ {
+ address: "index.docker.io:8080",
+ identifier: "index.docker.io:8080",
+ allIDs: []string{
+ "index.docker.io:8080",
+ "https://index.docker.io:8080", "http://index.docker.io:8080",
+ },
+ },
+ {
+ address: "foo.docker.io",
+ identifier: "foo.docker.io:443",
+ allIDs: []string{
+ "foo.docker.io:443", "https://foo.docker.io:443", "http://foo.docker.io:443",
+ "foo.docker.io", "https://foo.docker.io", "http://foo.docker.io",
+ },
+ },
+ {
+ address: "docker.io",
+ identifier: "https://index.docker.io/v1/",
+ allIDs: []string{"https://index.docker.io/v1/"},
+ },
+ {
+ address: "docker.io/whatever",
+ identifier: "docker.io:443",
+ allIDs: []string{
+ "docker.io:443", "https://docker.io:443", "http://docker.io:443",
+ "docker.io", "https://docker.io", "http://docker.io",
+ },
+ },
+ {
+ address: "http://docker.io",
+ identifier: "docker.io:443",
+ allIDs: []string{
+ "docker.io:443", "https://docker.io:443", "http://docker.io:443",
+ "docker.io", "https://docker.io", "http://docker.io",
+ },
+ },
+ {
+ address: "docker.io:80",
+ identifier: "docker.io:80",
+ allIDs: []string{
+ "docker.io:80",
+ "https://docker.io:80", "http://docker.io:80",
+ },
+ },
+ {
+ address: "docker.io:8080",
+ identifier: "docker.io:8080",
+ allIDs: []string{
+ "docker.io:8080",
+ "https://docker.io:8080", "http://docker.io:8080",
+ },
+ },
+ {
+ address: "anything/whatever?u=v&w=y;foo=bar#frag=o",
+ identifier: "anything:443",
+ allIDs: []string{
+ "anything:443", "https://anything:443", "http://anything:443",
+ "anything", "https://anything", "http://anything",
+ },
+ },
+ {
+ address: "https://registry-host.com/subpath/something?bar=bar&ns=registry-namespace.com&foo=foo",
+ identifier: "nerdctl-experimental://registry-namespace.com:443/host/registry-host.com:443/subpath/something",
+ allIDs: []string{
+ "nerdctl-experimental://registry-namespace.com:443/host/registry-host.com:443/subpath/something",
+ },
+ },
+ {
+ address: "localhost:1234",
+ identifier: "localhost:1234",
+ allIDs: []string{
+ "localhost:1234", "https://localhost:1234", "http://localhost:1234",
+ },
+ },
+ {
+ address: "127.0.0.1:1234",
+ identifier: "127.0.0.1:1234",
+ allIDs: []string{
+ "127.0.0.1:1234", "https://127.0.0.1:1234", "http://127.0.0.1:1234",
+ },
+ },
+ {
+ address: "[::1]:1234",
+ identifier: "[::1]:1234",
+ allIDs: []string{
+ "[::1]:1234", "https://[::1]:1234", "http://[::1]:1234",
+ },
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.address, func(t *testing.T) {
+ reg, err := Parse(tc.address)
+ assert.ErrorIs(t, err, tc.error)
+ if err == nil {
+ assert.Equal(t, reg.CanonicalIdentifier(), tc.identifier)
+ allIDs := reg.AllIdentifiers()
+ assert.Equal(t, len(allIDs), len(tc.allIDs))
+ for k, v := range tc.allIDs {
+ assert.Equal(t, allIDs[k], v)
+ }
+ }
+ })
+ }
+}
diff --git a/pkg/imgutil/fetch/fetch.go b/pkg/imgutil/fetch/fetch.go
new file mode 100644
index 00000000000..c9ab04f5533
--- /dev/null
+++ b/pkg/imgutil/fetch/fetch.go
@@ -0,0 +1,92 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package fetch
+
+import (
+ "context"
+ "io"
+
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+)
+
+// Config for content fetch
+type Config struct {
+ // Resolver
+ Resolver remotes.Resolver
+ // ProgressOutput to display progress
+ ProgressOutput io.Writer
+ // RemoteOpts, e.g. containerd.WithPullUnpack.
+ //
+ // Regardless to RemoteOpts, the following opts are always set:
+ // WithResolver, WithImageHandler, WithSchema1Conversion
+ //
+ // RemoteOpts related to unpacking can be set only when len(Platforms) is 1.
+ RemoteOpts []containerd.RemoteOpt
+ Platforms []ocispec.Platform // empty for all-platforms
+}
+
+func Fetch(ctx context.Context, client *containerd.Client, ref string, config *Config) error {
+ ongoing := jobs.New(ref)
+
+ pctx, stopProgress := context.WithCancel(ctx)
+ progress := make(chan struct{})
+
+ go func() {
+ if config.ProgressOutput != nil {
+ // no progress bar, because it hides some debug logs
+ jobs.ShowProgress(pctx, ongoing, client.ContentStore(), config.ProgressOutput)
+ }
+ close(progress)
+ }()
+
+ h := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
+ if desc.MediaType != images.MediaTypeDockerSchema1Manifest {
+ ongoing.Add(desc)
+ }
+ return nil, nil
+ })
+
+ log.G(pctx).WithField("image", ref).Debug("fetching")
+ platformMC := platformutil.NewMatchComparerFromOCISpecPlatformSlice(config.Platforms)
+ opts := []containerd.RemoteOpt{
+ containerd.WithResolver(config.Resolver),
+ containerd.WithImageHandler(h),
+ //nolint:staticcheck
+ containerd.WithSchema1Conversion, //lint:ignore SA1019 nerdctl should support schema1 as well.
+ containerd.WithPlatformMatcher(platformMC),
+ }
+ opts = append(opts, config.RemoteOpts...)
+
+ // Note that client.Fetch does not unpack
+ _, err := client.Fetch(pctx, ref, opts...)
+
+ stopProgress()
+ if err != nil {
+ return err
+ }
+
+ <-progress
+ return nil
+}
diff --git a/pkg/imgutil/filtering.go b/pkg/imgutil/filtering.go
index 3e1445c8c87..5555380ba4c 100644
--- a/pkg/imgutil/filtering.go
+++ b/pkg/imgutil/filtering.go
@@ -18,36 +18,47 @@ package imgutil
import (
"context"
+ "errors"
"fmt"
"regexp"
"strings"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- dockerreference "github.com/containerd/containerd/reference/docker"
- "github.com/containerd/nerdctl/pkg/referenceutil"
- "github.com/sirupsen/logrus"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
// Filter types supported to filter images.
-var (
+const (
FilterBeforeType = "before"
FilterSinceType = "since"
+ FilterUntilType = "until"
FilterLabelType = "label"
FilterReferenceType = "reference"
FilterDanglingType = "dangling"
)
+var (
+ errMultipleUntilFilters = errors.New("more than one until filter provided")
+ errNoUntilTimestamp = errors.New("no until timestamp provided")
+ errUnparsableUntilTimestamp = errors.New("unable to parse until timestamp")
+)
+
// Filters contains all types of filters to filter images.
type Filters struct {
Before []string
Since []string
+ Until string
Labels map[string]string
Reference []string
Dangling *bool
}
+type Filter func([]images.Image) ([]images.Image, error)
+
// ParseFilters parse filter strings.
func ParseFilters(filters []string) (*Filters, error) {
f := &Filters{Labels: make(map[string]string)}
@@ -68,20 +79,27 @@ func ParseFilters(filters []string) (*Filters, error) {
}
f.Dangling = &isDangling
} else if tempFilterToken[0] == FilterBeforeType {
- canonicalRef, err := referenceutil.ParseAny(tempFilterToken[1])
+ parsedReference, err := referenceutil.Parse(tempFilterToken[1])
if err != nil {
return nil, err
}
- f.Before = append(f.Before, fmt.Sprintf("name==%s", canonicalRef.String()))
+ f.Before = append(f.Before, fmt.Sprintf("name==%s", parsedReference.String()))
f.Before = append(f.Before, fmt.Sprintf("name==%s", tempFilterToken[1]))
} else if tempFilterToken[0] == FilterSinceType {
- canonicalRef, err := referenceutil.ParseAny(tempFilterToken[1])
+ parsedReference, err := referenceutil.Parse(tempFilterToken[1])
if err != nil {
return nil, err
}
- f.Since = append(f.Since, fmt.Sprintf("name==%s", canonicalRef.String()))
+ f.Since = append(f.Since, fmt.Sprintf("name==%s", parsedReference.String()))
f.Since = append(f.Since, fmt.Sprintf("name==%s", tempFilterToken[1]))
+ } else if tempFilterToken[0] == FilterUntilType {
+ if len(tempFilterToken[0]) == 0 {
+ return nil, errNoUntilTimestamp
+ } else if len(f.Until) > 0 {
+ return nil, errMultipleUntilFilters
+ }
+ f.Until = tempFilterToken[1]
} else if tempFilterToken[0] == FilterLabelType {
// To support filtering labels by keys.
f.Labels[tempFilterToken[1]] = ""
@@ -103,103 +121,235 @@ func ParseFilters(filters []string) (*Filters, error) {
return f, nil
}
-// FilterImages returns images in `labelImages` that are created
-// before MAX(beforeImages.CreatedAt) and after MIN(sinceImages.CreatedAt).
-func FilterImages(labelImages []images.Image, beforeImages []images.Image, sinceImages []images.Image) []images.Image {
- var filteredImages []images.Image
- maxTime := time.Now()
- minTime := time.Date(1970, time.Month(1), 1, 0, 0, 0, 0, time.UTC)
- if len(beforeImages) > 0 {
- maxTime = beforeImages[0].CreatedAt
- for _, value := range beforeImages {
- if value.CreatedAt.After(maxTime) {
- maxTime = value.CreatedAt
- }
+// ApplyFilters applies each filter function in the order provided
+// and returns the resulting filtered image list.
+func ApplyFilters(imageList []images.Image, filters ...Filter) ([]images.Image, error) {
+ var err error
+ for _, filter := range filters {
+ imageList, err = filter(imageList)
+ if err != nil {
+ return []images.Image{}, err
}
}
- if len(sinceImages) > 0 {
- minTime = sinceImages[0].CreatedAt
- for _, value := range sinceImages {
- if value.CreatedAt.Before(minTime) {
- minTime = value.CreatedAt
+ return imageList, nil
+}
+
+// FilterByCreatedAt filters an image list to images created before MAX(before..CreatedAt)
+// and after MIN(since..CreatedAt).
+func FilterByCreatedAt(ctx context.Context, client *containerd.Client, before []string, since []string) Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ var (
+ minTime = time.Date(1970, time.Month(1), 1, 0, 0, 0, 0, time.UTC)
+ maxTime = time.Now()
+ )
+
+ fetchImageNames := func(names []string) string {
+ parsedNames := make([]string, 0, len(names))
+ for _, name := range names {
+ parsedNames = append(parsedNames, strings.TrimPrefix(name, "name=="))
}
+ return strings.Join(parsedNames, ",")
}
- }
- for _, image := range labelImages {
- if image.CreatedAt.After(minTime) && image.CreatedAt.Before(maxTime) {
- filteredImages = append(filteredImages, image)
- }
- }
- return filteredImages
-}
-// FilterByReference filters images using references given in `filters`.
-func FilterByReference(imageList []images.Image, filters []string) ([]images.Image, error) {
- var filteredImageList []images.Image
- logrus.Debug(filters)
- for _, image := range imageList {
- logrus.Debug(image.Name)
- var matches int
- for _, f := range filters {
- var ref dockerreference.Reference
- var err error
- ref, err = dockerreference.ParseAnyReference(image.Name)
+ imageStore := client.ImageService()
+ if len(before) > 0 {
+ beforeImages, err := imageStore.List(ctx, before...)
if err != nil {
- return nil, fmt.Errorf("unable to parse image name: %s while filtering by reference because of %s", image.Name, err.Error())
+ return []images.Image{}, err
}
+ if len(beforeImages) == 0 {
+ //nolint:stylecheck
+ return []images.Image{}, fmt.Errorf("No such image: %s", fetchImageNames(before))
+ }
+ maxTime = beforeImages[0].CreatedAt
+ for _, image := range beforeImages {
+ if image.CreatedAt.After(maxTime) {
+ maxTime = image.CreatedAt
+ }
+ }
+ }
- familiarMatch, err := dockerreference.FamiliarMatch(f, ref)
+ if len(since) > 0 {
+ sinceImages, err := imageStore.List(ctx, since...)
if err != nil {
- return nil, err
+ return []images.Image{}, err
}
- regexpMatch, err := regexp.MatchString(f, image.Name)
- if err != nil {
- return nil, err
+ if len(sinceImages) == 0 {
+ //nolint:stylecheck
+ return []images.Image{}, fmt.Errorf("No such image: %s", fetchImageNames(since))
}
- if familiarMatch || regexpMatch {
- matches++
+ minTime = sinceImages[0].CreatedAt
+ for _, image := range sinceImages {
+ if image.CreatedAt.Before(minTime) {
+ minTime = image.CreatedAt
+ }
}
}
- if matches == len(filters) {
- filteredImageList = append(filteredImageList, image)
- }
+
+ return filter(imageList, func(i images.Image) (bool, error) {
+ return imageCreatedBetween(i, minTime, maxTime), nil
+ })
}
- return filteredImageList, nil
}
-// FilterDangling filters dangling images (or keeps if `dangling` == false).
-func FilterDangling(imageList []images.Image, dangling bool) []images.Image {
- var filtered []images.Image
- for _, image := range imageList {
- _, tag := ParseRepoTag(image.Name)
+// FilterUntil filters images created before the provided timestamp.
+func FilterUntil(until string) Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ if len(until) == 0 {
+ return []images.Image{}, errNoUntilTimestamp
+ }
+
+ var (
+ parsedTime time.Time
+ err error
+ )
- if dangling && tag == "" {
- filtered = append(filtered, image)
+ type parseUntilFunc func(string) (time.Time, error)
+ parsingFuncs := []parseUntilFunc{
+ func(until string) (time.Time, error) {
+ return time.Parse(time.RFC3339, until)
+ },
+ func(until string) (time.Time, error) {
+ return time.Parse(time.RFC3339Nano, until)
+ },
+ func(until string) (time.Time, error) {
+ return time.Parse(time.DateOnly, until)
+ },
+ func(until string) (time.Time, error) {
+ // Go duration strings
+ d, err := time.ParseDuration(until)
+ if err != nil {
+ return time.Time{}, err
+ }
+ return time.Now().Add(-d), nil
+ },
}
- if !dangling && tag != "" {
- filtered = append(filtered, image)
+
+ for _, parse := range parsingFuncs {
+ parsedTime, err = parse(until)
+ if err != nil {
+ continue
+ }
+ break
}
+
+ if err != nil {
+ return []images.Image{}, errUnparsableUntilTimestamp
+ }
+
+ return filter(imageList, func(i images.Image) (bool, error) {
+ return imageCreatedBefore(i, parsedTime), nil
+ })
}
- return filtered
}
-// FilterByLabel filters images based on labels given in `filters`.
-func FilterByLabel(ctx context.Context, client *containerd.Client, imageList []images.Image, filters map[string]string) ([]images.Image, error) {
- for lk, lv := range filters {
- var imageLabels []images.Image
- for _, img := range imageList {
- ci := containerd.NewImage(client, img)
- cfg, _, err := ReadImageConfig(ctx, ci)
+// FilterByLabel filters an image list based on labels applied to the image's config specification for the platform.
+// Any matching label will include the image in the list.
+func FilterByLabel(ctx context.Context, client *containerd.Client, labels map[string]string) Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ return filter(imageList, func(i images.Image) (bool, error) {
+ clientImage := containerd.NewImage(client, i)
+ imageCfg, _, err := ReadImageConfig(ctx, clientImage)
if err != nil {
- return nil, err
+ // Stop-gap measure. Do not hard error if some images config cannot be read.
+ // See https://github.com/containerd/nerdctl/issues/3516
+ log.G(ctx).WithError(err).Errorf("failed reading image config for %s (%s)", clientImage.Name(), clientImage.Platform())
+ return false, nil
}
- if val, ok := cfg.Config.Labels[lk]; ok {
- if val == lv || lv == "" {
- imageLabels = append(imageLabels, img)
- }
+ return matchesAllLabels(imageCfg.Config.Labels, labels), nil
+ })
+ }
+}
+
+// FilterByReference filters an image list based on
+// matching the provided reference patterns
+func FilterByReference(referencePatterns []string) Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ return filter(imageList, func(i images.Image) (bool, error) {
+ return matchesReferences(i, referencePatterns)
+ })
+ }
+}
+
+// FilterDanglingImages filters an image list for dangling (untagged) images.
+func FilterDanglingImages() Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ return filter(imageList, func(i images.Image) (bool, error) {
+ return isDangling(i), nil
+ })
+ }
+}
+
+// FilterTaggedImages filters an image list for tagged images.
+func FilterTaggedImages() Filter {
+ return func(imageList []images.Image) ([]images.Image, error) {
+ return filter(imageList, func(i images.Image) (bool, error) {
+ return !isDangling(i), nil
+ })
+ }
+}
+
+func filter[T any](items []T, f func(item T) (bool, error)) ([]T, error) {
+ filteredItems := make([]T, 0, len(items))
+ for _, item := range items {
+ ok, err := f(item)
+ if err != nil {
+ return []T{}, err
+ } else if ok {
+ filteredItems = append(filteredItems, item)
+ }
+ }
+ return filteredItems, nil
+}
+
+func imageCreatedBetween(image images.Image, minTime time.Time, maxTime time.Time) bool {
+ return image.CreatedAt.After(minTime) && image.CreatedAt.Before(maxTime)
+}
+
+func imageCreatedBefore(image images.Image, maxTime time.Time) bool {
+ return image.CreatedAt.Before(maxTime)
+}
+
+func matchesAllLabels(imageCfgLabels map[string]string, filterLabels map[string]string) bool {
+ var matches int
+ for lk, lv := range filterLabels {
+ if val, ok := imageCfgLabels[lk]; ok {
+ if val == lv || lv == "" {
+ matches++
}
}
- imageList = imageLabels
}
- return imageList, nil
+ return matches == len(filterLabels)
+}
+
+func matchesReferences(image images.Image, referencePatterns []string) (bool, error) {
+ var matches int
+
+ parsedReference, err := referenceutil.Parse(image.Name)
+ if err != nil {
+ return false, err
+ }
+
+ for _, pattern := range referencePatterns {
+ familiarMatch, err := parsedReference.FamiliarMatch(pattern)
+ if err != nil {
+ return false, err
+ }
+
+ regexpMatch, err := regexp.MatchString(pattern, image.Name)
+ if err != nil {
+ return false, err
+ }
+
+ if familiarMatch || regexpMatch {
+ matches++
+ }
+ }
+
+ return matches == len(referencePatterns), nil
+}
+
+func isDangling(image images.Image) bool {
+ _, tag := ParseRepoTag(image.Name)
+ return tag == ""
}
diff --git a/pkg/imgutil/filtering_test.go b/pkg/imgutil/filtering_test.go
new file mode 100644
index 00000000000..7d82cb2ce60
--- /dev/null
+++ b/pkg/imgutil/filtering_test.go
@@ -0,0 +1,491 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package imgutil
+
+import (
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/containerd/v2/core/images"
+)
+
+func TestApplyFilters(t *testing.T) {
+ tests := []struct {
+ name string
+ images []images.Image
+ filters []Filter
+ expectedImages []images.Image
+ expectedErr error
+ }{
+ {
+ name: "EmptyList",
+ images: []images.Image{},
+ filters: []Filter{
+ FilterDanglingImages(),
+ },
+ expectedImages: []images.Image{},
+ },
+ {
+ name: "ApplyNoFilters",
+ images: []images.Image{
+ {
+ Name: "",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ },
+ filters: []Filter{},
+ expectedImages: []images.Image{
+ {
+ Name: "",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ },
+ },
+ {
+ name: "ApplySingleFilter",
+ images: []images.Image{
+ {
+ Name: "",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ },
+ filters: []Filter{
+ FilterDanglingImages(),
+ },
+ expectedImages: []images.Image{
+ {
+ Name: "",
+ },
+ },
+ },
+ {
+ name: "ApplyMultipleFilters",
+ images: []images.Image{
+ {
+ Name: "",
+ },
+ {
+ Name: "alpine:3.19",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "public.ecr.aws/docker/library/hello-world:latest",
+ },
+ },
+ filters: []Filter{
+ FilterTaggedImages(),
+ FilterByReference([]string{"hello-world"}),
+ },
+ expectedImages: []images.Image{
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "public.ecr.aws/docker/library/hello-world:latest",
+ },
+ },
+ },
+ {
+ name: "ReturnErrorAndEmptyListOnFilterError",
+ images: []images.Image{
+ {
+ Name: ":",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ },
+ filters: []Filter{
+ FilterDanglingImages(),
+ FilterUntil(""),
+ },
+ expectedImages: []images.Image{},
+ expectedErr: errNoUntilTimestamp,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actualImages, err := ApplyFilters(test.images, test.filters...)
+ if test.expectedErr == nil {
+ assert.NilError(t, err)
+ } else {
+ assert.ErrorIs(t, err, test.expectedErr)
+ }
+ assert.Equal(t, len(actualImages), len(test.expectedImages))
+ assert.DeepEqual(t, actualImages, test.expectedImages)
+ })
+ }
+}
+
+func TestFilterUntil(t *testing.T) {
+ now := time.Now().UTC()
+
+ tests := []struct {
+ name string
+ until string
+ images []images.Image
+ expectedImages []images.Image
+ expectedErr error
+ }{
+ {
+ name: "EmptyTimestampReturnsError",
+ until: "",
+ images: []images.Image{},
+ expectedImages: []images.Image{},
+ expectedErr: errNoUntilTimestamp,
+ },
+ {
+ name: "UnparseableTimestampReturnsError",
+ until: "-2006-01-02T15:04:05Z07:00",
+ images: []images.Image{},
+ expectedImages: []images.Image{},
+ expectedErr: errUnparsableUntilTimestamp,
+ },
+ {
+ name: "ImagesOlderThan3Hours(Go duration)",
+ until: "3h",
+ images: []images.Image{
+ {
+ Name: "image:yesterday",
+ CreatedAt: now.Add(-24 * time.Hour),
+ },
+ {
+ Name: "image:today",
+ CreatedAt: now.Add(-12 * time.Hour),
+ },
+ {
+ Name: "image:latest",
+ CreatedAt: now,
+ },
+ },
+ expectedImages: []images.Image{
+ {
+ Name: "image:yesterday",
+ CreatedAt: now.Add(-24 * time.Hour),
+ },
+ {
+ Name: "image:today",
+ CreatedAt: now.Add(-12 * time.Hour),
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actualImages, err := FilterUntil(test.until)(test.images)
+ if test.expectedErr == nil {
+ assert.NilError(t, err)
+ } else {
+ assert.ErrorIs(t, err, test.expectedErr)
+ }
+ assert.Equal(t, len(actualImages), len(test.expectedImages))
+ assert.DeepEqual(t, actualImages, test.expectedImages)
+ })
+ }
+}
+
+func TestFilterByReference(t *testing.T) {
+ tests := []struct {
+ name string
+ referencePatterns []string
+ images []images.Image
+ expectedImages []images.Image
+ expectedErr error
+ }{
+ {
+ name: "EmptyList",
+ images: []images.Image{},
+ expectedImages: []images.Image{},
+ },
+ {
+ name: "MatchByReference",
+ images: []images.Image{
+ {
+ Name: "foo:latest",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "public.ecr.aws/docker/library/hello-world:latest",
+ },
+ },
+ referencePatterns: []string{"hello-world"},
+ expectedImages: []images.Image{
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "public.ecr.aws/docker/library/hello-world:latest",
+ },
+ },
+ },
+ {
+ name: "NoMatchExists",
+ images: []images.Image{
+ {
+ Name: "foo:latest",
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "public.ecr.aws/docker/library/hello-world:latest",
+ },
+ },
+ referencePatterns: []string{"foobar"},
+ expectedImages: []images.Image{},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actualImages, err := FilterByReference(test.referencePatterns)(test.images)
+ if test.expectedErr == nil {
+ assert.NilError(t, err)
+ } else {
+ assert.ErrorIs(t, err, test.expectedErr)
+ }
+ assert.Equal(t, len(actualImages), len(test.expectedImages))
+ assert.DeepEqual(t, actualImages, test.expectedImages)
+ })
+ }
+}
+
+func TestFilterDanglingImages(t *testing.T) {
+ tests := []struct {
+ name string
+ dangling bool
+ images []images.Image
+ expectedImages []images.Image
+ }{
+ {
+ name: "EmptyList",
+ dangling: true,
+ images: []images.Image{},
+ expectedImages: []images.Image{},
+ },
+ {
+ name: "IsDangling",
+ dangling: true,
+ images: []images.Image{
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling1"},
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling2"},
+ },
+ },
+ expectedImages: []images.Image{
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling1"},
+ },
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling2"},
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actualImages, err := FilterDanglingImages()(test.images)
+ assert.NilError(t, err)
+ assert.Equal(t, len(actualImages), len(test.expectedImages))
+ assert.DeepEqual(t, actualImages, test.expectedImages)
+ })
+ }
+}
+
+func TestFilterTaggedImages(t *testing.T) {
+ tests := []struct {
+ name string
+ dangling bool
+ images []images.Image
+ expectedImages []images.Image
+ }{
+ {
+ name: "EmptyList",
+ dangling: true,
+ images: []images.Image{},
+ expectedImages: []images.Image{},
+ },
+ {
+ name: "IsTagged",
+ dangling: true,
+ images: []images.Image{
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling1"},
+ },
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ {
+ Name: "",
+ Labels: map[string]string{"ref": "dangling2"},
+ },
+ },
+ expectedImages: []images.Image{
+ {
+ Name: "docker.io/library/hello-world:latest",
+ },
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ actualImages, err := FilterTaggedImages()(test.images)
+ assert.NilError(t, err)
+ assert.Equal(t, len(actualImages), len(test.expectedImages))
+ assert.DeepEqual(t, actualImages, test.expectedImages)
+ })
+ }
+}
+
+func TestImageCreatedBetween(t *testing.T) {
+ var (
+ unixEpoch = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
+ y2k = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ now = time.Now()
+ )
+ tests := []struct {
+ name string
+ image images.Image
+ lhs time.Time
+ rhs time.Time
+ fallsBetween bool
+ }{
+ {
+ name: "PreviousImage",
+ image: images.Image{
+ CreatedAt: unixEpoch,
+ },
+ lhs: y2k,
+ rhs: now,
+ fallsBetween: false,
+ },
+ {
+ name: "AfterImage",
+ image: images.Image{
+ CreatedAt: now,
+ },
+ lhs: unixEpoch,
+ rhs: y2k,
+ fallsBetween: false,
+ },
+ {
+ name: "InBetweenTimeImage",
+ image: images.Image{
+ CreatedAt: y2k,
+ },
+ lhs: unixEpoch,
+ rhs: now,
+ fallsBetween: true,
+ },
+ {
+ name: "ExclusiveLeft",
+ image: images.Image{
+ CreatedAt: unixEpoch,
+ },
+ lhs: unixEpoch,
+ rhs: now,
+ fallsBetween: false,
+ },
+ {
+ name: "ExclusiveRight",
+ image: images.Image{
+ CreatedAt: now,
+ },
+ lhs: unixEpoch,
+ rhs: now,
+ fallsBetween: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, imageCreatedBetween(test.image, test.lhs, test.rhs), test.fallsBetween)
+ })
+ }
+}
+
+func TestMatchesAnyLabel(t *testing.T) {
+ tests := []struct {
+ name string
+ imageLabels map[string]string
+ labelsToMatch map[string]string
+ matches bool
+ }{
+ {
+ name: "ImageHasNoLabels",
+ imageLabels: map[string]string{},
+ labelsToMatch: map[string]string{"foo": "bar"},
+ matches: false,
+ },
+ {
+ name: "SingleMatchingLabel",
+ imageLabels: map[string]string{"org": "com.example.nerdctl"},
+ labelsToMatch: map[string]string{"org": "com.example.nerdctl"},
+ matches: true,
+ },
+ {
+ name: "KeyOnlyMatchingLabel",
+ imageLabels: map[string]string{"org": "com.example.nerdctl"},
+ labelsToMatch: map[string]string{"org": ""},
+ matches: true,
+ },
+ {
+ name: "KeyValueDoesNotMatch",
+ imageLabels: map[string]string{"org": "com.example.nerdctl"},
+ labelsToMatch: map[string]string{"org": "com.example.containerd"},
+ matches: false,
+ },
+ {
+ name: "AllMatchingLabel",
+ imageLabels: map[string]string{"org": "com.example.nerdctl", "foo": "bar"},
+ labelsToMatch: map[string]string{"org": "com.example.containerd", "foo": "bar"},
+ matches: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, matchesAllLabels(test.imageLabels, test.labelsToMatch), test.matches)
+ })
+ }
+}
diff --git a/pkg/imgutil/imgutil.go b/pkg/imgutil/imgutil.go
index 14e04c40705..3f8076df9f4 100644
--- a/pkg/imgutil/imgutil.go
+++ b/pkg/imgutil/imgutil.go
@@ -19,27 +19,31 @@ package imgutil
import (
"context"
"encoding/json"
+ "errors"
"fmt"
- "io"
+ "net/http"
"reflect"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/platforms"
- refdocker "github.com/containerd/containerd/reference/docker"
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/containerd/snapshots"
- "github.com/containerd/imgcrypt"
- "github.com/containerd/imgcrypt/images/encryption"
- "github.com/containerd/nerdctl/pkg/errutil"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
- "github.com/containerd/nerdctl/pkg/imgutil/pull"
- "github.com/docker/docker/errdefs"
"github.com/opencontainers/image-spec/identity"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/containerd/v2/core/snapshots"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/imgcrypt/v2"
+ "github.com/containerd/imgcrypt/v2/images/encryption"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/errutil"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/pull"
+ "github.com/containerd/nerdctl/v2/pkg/referenceutil"
)
// EnsuredImage contains the image existed in containerd and its metadata.
@@ -57,7 +61,7 @@ type PullMode = string
// GetExistingImage returns the specified image if exists in containerd. Return errdefs.NotFound() if not exists.
func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotter, rawRef string, platform ocispec.Platform) (*EnsuredImage, error) {
var res *EnsuredImage
- imagewalker := &imagewalker.ImageWalker{
+ imgwalker := &imagewalker.ImageWalker{
Client: client,
OnFound: func(ctx context.Context, found imagewalker.Found) error {
if res != nil {
@@ -85,15 +89,15 @@ func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotte
return nil
},
}
- count, err := imagewalker.Walk(ctx, rawRef)
+ count, err := imgwalker.Walk(ctx, rawRef)
if err != nil {
return nil, err
}
if count == 0 {
- return nil, errdefs.NotFound(fmt.Errorf("got count 0 after walking"))
+ return nil, errors.Join(errdefs.ErrNotFound, errors.New("got count 0 after walking"))
}
if res == nil {
- return nil, errdefs.NotFound(fmt.Errorf("got nil res after walking"))
+ return nil, errors.Join(errdefs.ErrNotFound, errors.New("got nil res after walking"))
}
return res, nil
}
@@ -101,64 +105,60 @@ func GetExistingImage(ctx context.Context, client *containerd.Client, snapshotte
// EnsureImage ensures the image.
//
// # When insecure is set, skips verifying certs, and also falls back to HTTP when the registry does not speak HTTPS
-//
-// FIXME: this func has too many args
-func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter, rawRef string, mode PullMode, insecure bool, hostsDirs []string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool) (*EnsuredImage, error) {
- switch mode {
+func EnsureImage(ctx context.Context, client *containerd.Client, rawRef string, options types.ImagePullOptions) (*EnsuredImage, error) {
+ switch options.Mode {
case "always", "missing", "never":
// NOP
default:
- return nil, fmt.Errorf("unexpected pull mode: %q", mode)
+ return nil, fmt.Errorf("unexpected pull mode: %q", options.Mode)
}
// if not `always` pull and given one platform and image found locally, return existing image directly.
- if mode != "always" && len(ocispecPlatforms) == 1 {
- if res, err := GetExistingImage(ctx, client, snapshotter, rawRef, ocispecPlatforms[0]); err == nil {
+ if options.Mode != "always" && len(options.OCISpecPlatform) == 1 {
+ if res, err := GetExistingImage(ctx, client, options.GOptions.Snapshotter, rawRef, options.OCISpecPlatform[0]); err == nil {
return res, nil
} else if !errdefs.IsNotFound(err) {
return nil, err
}
}
- if mode == "never" {
+ if options.Mode == "never" {
return nil, fmt.Errorf("image not available: %q", rawRef)
}
- named, err := refdocker.ParseDockerRef(rawRef)
+ parsedReference, err := referenceutil.Parse(rawRef)
if err != nil {
return nil, err
}
- ref := named.String()
- refDomain := refdocker.Domain(named)
var dOpts []dockerconfigresolver.Opt
- if insecure {
- logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain)
+ if options.GOptions.InsecureRegistry {
+ log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
- dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs))
- resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
+ dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(options.GOptions.HostsDir))
+ resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...)
if err != nil {
return nil, err
}
- img, err := PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet)
+ img, err := PullImage(ctx, client, resolver, parsedReference.String(), options)
if err != nil {
// In some circumstance (e.g. people just use 80 port to support pure http), the error will contain message like "dial tcp : connection refused".
- if !errutil.IsErrHTTPResponseToHTTPSClient(err) && !errutil.IsErrConnectionRefused(err) {
+ if !errors.Is(err, http.ErrSchemeMismatch) && !errutil.IsErrConnectionRefused(err) {
return nil, err
}
- if insecure {
- logrus.WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", refDomain)
+ if options.GOptions.InsecureRegistry {
+ log.G(ctx).WithError(err).Warnf("server %q does not seem to support HTTPS, falling back to plain HTTP", parsedReference.Domain)
dOpts = append(dOpts, dockerconfigresolver.WithPlainHTTP(true))
- resolver, err = dockerconfigresolver.New(ctx, refDomain, dOpts...)
+ resolver, err = dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...)
if err != nil {
return nil, err
}
- return PullImage(ctx, client, stdout, stderr, snapshotter, resolver, ref, ocispecPlatforms, unpack, quiet)
+ return PullImage(ctx, client, resolver, parsedReference.String(), options)
}
- logrus.WithError(err).Errorf("server %q does not seem to support HTTPS", refDomain)
- logrus.Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
+ log.G(ctx).WithError(err).Errorf("server %q does not seem to support HTTPS", parsedReference.Domain)
+ log.G(ctx).Info("Hint: you may want to try --insecure-registry to allow plain HTTP (if you are in a trusted network)")
return nil, err
}
@@ -167,25 +167,23 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr
// ResolveDigest resolves `rawRef` and returns its descriptor digest.
func ResolveDigest(ctx context.Context, rawRef string, insecure bool, hostsDirs []string) (string, error) {
- named, err := refdocker.ParseDockerRef(rawRef)
+ parsedReference, err := referenceutil.Parse(rawRef)
if err != nil {
return "", err
}
- ref := named.String()
- refDomain := refdocker.Domain(named)
var dOpts []dockerconfigresolver.Opt
if insecure {
- logrus.Warnf("skipping verifying HTTPS certs for %q", refDomain)
+ log.G(ctx).Warnf("skipping verifying HTTPS certs for %q", parsedReference.Domain)
dOpts = append(dOpts, dockerconfigresolver.WithSkipVerifyCerts(true))
}
dOpts = append(dOpts, dockerconfigresolver.WithHostsDirs(hostsDirs))
- resolver, err := dockerconfigresolver.New(ctx, refDomain, dOpts...)
+ resolver, err := dockerconfigresolver.New(ctx, parsedReference.Domain, dOpts...)
if err != nil {
return "", err
}
- _, desc, err := resolver.Resolve(ctx, ref)
+ _, desc, err := resolver.Resolve(ctx, parsedReference.String())
if err != nil {
return "", err
}
@@ -194,7 +192,7 @@ func ResolveDigest(ctx context.Context, rawRef string, insecure bool, hostsDirs
}
// PullImage pulls an image using the specified resolver.
-func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter string, resolver remotes.Resolver, ref string, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool) (*EnsuredImage, error) {
+func PullImage(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, ref string, options types.ImagePullOptions) (*EnsuredImage, error) {
ctx, done, err := client.WithLease(ctx)
if err != nil {
return nil, err
@@ -205,24 +203,27 @@ func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io
config := &pull.Config{
Resolver: resolver,
RemoteOpts: []containerd.RemoteOpt{},
- Platforms: ocispecPlatforms, // empty for all-platforms
+ Platforms: options.OCISpecPlatform, // empty for all-platforms
}
- if !quiet {
- config.ProgressOutput = stderr
+ if !options.Quiet {
+ config.ProgressOutput = options.Stderr
+ if options.ProgressOutputToStdout {
+ config.ProgressOutput = options.Stdout
+ }
}
// unpack(B) if given 1 platform unless specified by `unpack`
- unpackB := len(ocispecPlatforms) == 1
- if unpack != nil {
- unpackB = *unpack
- if unpackB && len(ocispecPlatforms) != 1 {
+ unpackB := len(options.OCISpecPlatform) == 1
+ if options.Unpack != nil {
+ unpackB = *options.Unpack
+ if unpackB && len(options.OCISpecPlatform) != 1 {
return nil, fmt.Errorf("unpacking requires a single platform to be specified (e.g., --platform=amd64)")
}
}
- snOpt := getSnapshotterOpts(snapshotter)
+ snOpt := getSnapshotterOpts(options.GOptions.Snapshotter)
if unpackB {
- logrus.Debugf("The image will be unpacked for platform %q, snapshotter %q.", ocispecPlatforms[0], snapshotter)
+ log.G(ctx).Debugf("The image will be unpacked for platform %q, snapshotter %q.", options.OCISpecPlatform[0], options.GOptions.Snapshotter)
imgcryptPayload := imgcrypt.Payload{}
imgcryptUnpackOpt := encryption.WithUnpackConfigApplyOpts(encryption.WithDecryptedUnpack(&imgcryptPayload))
config.RemoteOpts = append(config.RemoteOpts,
@@ -230,9 +231,9 @@ func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io
containerd.WithUnpackOpts([]containerd.UnpackOpt{imgcryptUnpackOpt}))
// different remote snapshotters will update pull.Config separately
- snOpt.apply(config, ref)
+ snOpt.apply(config, ref, options.RFlags)
} else {
- logrus.Debugf("The image will not be unpacked. Platforms=%v.", ocispecPlatforms)
+ log.G(ctx).Debugf("The image will not be unpacked. Platforms=%v.", options.OCISpecPlatform)
}
containerdImage, err = pull.Pull(ctx, client, ref, config)
@@ -247,7 +248,7 @@ func PullImage(ctx context.Context, client *containerd.Client, stdout, stderr io
Ref: ref,
Image: containerdImage,
ImageConfig: *imgConfig,
- Snapshotter: snapshotter,
+ Snapshotter: options.GOptions.Snapshotter,
Remote: snOpt.isRemote(),
}
return res, nil
@@ -358,45 +359,47 @@ func ReadImageConfig(ctx context.Context, img containerd.Image) (ocispec.Image,
// ParseRepoTag parses raw `imgName` to repository and tag.
func ParseRepoTag(imgName string) (string, string) {
- logrus.Debugf("raw image name=%q", imgName)
+ log.L.Debugf("raw image name=%q", imgName)
- ref, err := refdocker.ParseDockerRef(imgName)
+ parsedReference, err := referenceutil.Parse(imgName)
if err != nil {
- logrus.WithError(err).Debugf("unparsable image name %q", imgName)
+ log.L.WithError(err).Debugf("unparsable image name %q", imgName)
return "", ""
}
- var tag string
-
- if tagged, ok := ref.(refdocker.Tagged); ok {
- tag = tagged.Tag()
- }
- repository := refdocker.FamiliarName(ref)
-
- return repository, tag
+ return parsedReference.FamiliarName(), parsedReference.Tag
}
-type snapshotKey string
-
-// recursive function to calculate total usage of key's parent
-func (key snapshotKey) add(ctx context.Context, s snapshots.Snapshotter, usage *snapshots.Usage) error {
- if key == "" {
- return nil
- }
- u, err := s.Usage(ctx, string(key))
- if err != nil {
- return err
- }
-
- usage.Add(u)
+// ResourceUsage will return:
+// - the Usage value of the resource referenced by ID
+// - the cumulative Usage value of the resource, and all parents, recursively
+// Typically, for a running container, this will equal the size of the read-write layer, plus the sum of the size of all layers in the base image
+func ResourceUsage(ctx context.Context, snapshotter snapshots.Snapshotter, resourceID string) (snapshots.Usage, snapshots.Usage, error) {
+ first := snapshots.Usage{}
+ total := snapshots.Usage{}
+ var info snapshots.Info
+ for next := resourceID; next != ""; next = info.Parent {
+ // Get the resource usage info
+ usage, err := snapshotter.Usage(ctx, next)
+ if err != nil {
+ return first, total, err
+ }
+ // In case that's the first one, store that
+ if next == resourceID {
+ first = usage
+ }
+ // And increment totals
+ total.Size += usage.Size
+ total.Inodes += usage.Inodes
- info, err := s.Stat(ctx, string(key))
- if err != nil {
- return err
+ // Now, get the parent, if any and iterate
+ info, err = snapshotter.Stat(ctx, next)
+ if err != nil {
+ return first, total, err
+ }
}
- key = snapshotKey(info.Parent)
- return key.add(ctx, s, usage)
+ return first, total, nil
}
// UnpackedImageSize is the size of the unpacked snapshots.
@@ -408,23 +411,56 @@ func UnpackedImageSize(ctx context.Context, s snapshots.Snapshotter, img contain
}
chainID := identity.ChainID(diffIDs).String()
- usage, err := s.Usage(ctx, chainID)
+ _, total, err := ResourceUsage(ctx, s, chainID)
+
+ return total.Size, err
+}
+
+// GetUnusedImages returns the list of all images which are not referenced by a container.
+func GetUnusedImages(ctx context.Context, client *containerd.Client, filters ...Filter) ([]images.Image, error) {
+ var (
+ imageStore = client.ImageService()
+ containerStore = client.ContainerService()
+ )
+
+ containers, err := containerStore.List(ctx)
if err != nil {
- if errdefs.IsNotFound(err) {
- logrus.WithError(err).Debugf("image %q seems not unpacked", img.Name())
- return 0, nil
- }
- return 0, err
+ return []images.Image{}, err
+ }
+
+ usedImages := make(map[string]struct{})
+ for _, container := range containers {
+ usedImages[container.Image] = struct{}{}
}
- info, err := s.Stat(ctx, chainID)
+ allImages, err := imageStore.List(ctx)
if err != nil {
- return 0, err
+ return []images.Image{}, err
}
- //add ChainID's parent usage to the total usage
- if err := snapshotKey(info.Parent).add(ctx, s, &usage); err != nil {
- return 0, err
+ unusedImages := make([]images.Image, 0, len(allImages))
+ for _, image := range allImages {
+ if _, ok := usedImages[image.Name]; ok {
+ continue
+ }
+ unusedImages = append(unusedImages, image)
}
- return usage.Size, nil
+
+ return ApplyFilters(unusedImages, filters...)
+}
+
+// GetDanglingImages returns the list of all images which are not tagged.
+func GetDanglingImages(ctx context.Context, client *containerd.Client, filters ...Filter) ([]images.Image, error) {
+ var (
+ imageStore = client.ImageService()
+ )
+
+ allImages, err := imageStore.List(ctx)
+ if err != nil {
+ return []images.Image{}, err
+ }
+
+ filters = append([]Filter{FilterDanglingImages()}, filters...)
+
+ return ApplyFilters(allImages, filters...)
}
diff --git a/pkg/imgutil/jobs/jobs.go b/pkg/imgutil/jobs/jobs.go
index da5da32c4ef..7dc0d985828 100644
--- a/pkg/imgutil/jobs/jobs.go
+++ b/pkg/imgutil/jobs/jobs.go
@@ -24,13 +24,14 @@ import (
"text/tabwriter"
"time"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/log"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/containerd/remotes"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/containerd/v2/pkg/progress"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
)
// ShowProgress continuously updates the output with job progress
@@ -101,11 +102,10 @@ outer:
if !errdefs.IsNotFound(err) {
log.G(ctx).WithError(err).Error("failed to get content info")
continue outer
- } else {
- statuses[key] = StatusInfo{
- Ref: key,
- Status: StatusWaiting,
- }
+ }
+ statuses[key] = StatusInfo{
+ Ref: key,
+ Status: StatusWaiting,
}
} else if info.CreatedAt.After(start) {
statuses[key] = StatusInfo{
diff --git a/pkg/imgutil/load/load.go b/pkg/imgutil/load/load.go
new file mode 100644
index 00000000000..0afb322f4e4
--- /dev/null
+++ b/pkg/imgutil/load/load.go
@@ -0,0 +1,151 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package load
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/archive"
+ "github.com/containerd/containerd/v2/pkg/archive/compression"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
+)
+
+// FromArchive loads and unpacks the images from the tar archive specified in image load options.
+func FromArchive(ctx context.Context, client *containerd.Client, options types.ImageLoadOptions) ([]images.Image, error) {
+ if options.Input != "" {
+ f, err := os.Open(options.Input)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ options.Stdin = f
+ } else {
+ // check if stdin is empty.
+ stdinStat, err := os.Stdin.Stat()
+ if err != nil {
+ return nil, err
+ }
+ if stdinStat.Size() == 0 && (stdinStat.Mode()&os.ModeNamedPipe) == 0 {
+ return nil, errors.New("stdin is empty and input flag is not specified")
+ }
+ }
+ decompressor, err := compression.DecompressStream(options.Stdin)
+ if err != nil {
+ return nil, err
+ }
+ platMC, err := platformutil.NewMatchComparer(options.AllPlatforms, options.Platform)
+ if err != nil {
+ return nil, err
+ }
+ imgs, err := importImages(ctx, client, decompressor, options.GOptions.Snapshotter, platMC)
+ if err != nil {
+ return nil, err
+ }
+ unpackedImages := make([]images.Image, 0, len(imgs))
+ for _, img := range imgs {
+ err := unpackImage(ctx, client, img, platMC, options)
+ if err != nil {
+ return unpackedImages, fmt.Errorf("error unpacking image (%s): %w", img.Name, err)
+ }
+ unpackedImages = append(unpackedImages, img)
+ }
+ return unpackedImages, nil
+}
+
+// FromOCIArchive loads and unpacks the images from the OCI formatted archive at the provided file system path.
+func FromOCIArchive(ctx context.Context, client *containerd.Client, pathToOCIArchive string, options types.ImageLoadOptions) ([]images.Image, error) {
+ const ociArchivePrefix = "oci-archive://"
+ pathToOCIArchive = strings.TrimPrefix(pathToOCIArchive, ociArchivePrefix)
+
+ const separator = ":"
+ if strings.Contains(pathToOCIArchive, separator) {
+ subs := strings.Split(pathToOCIArchive, separator)
+ if len(subs) != 2 {
+ return nil, errors.New("too many seperators found in oci-archive path")
+ }
+ pathToOCIArchive = subs[0]
+ }
+
+ options.Input = pathToOCIArchive
+
+ return FromArchive(ctx, client, options)
+}
+
+type readCounter struct {
+ io.Reader
+ N int
+}
+
+func (r *readCounter) Read(p []byte) (int, error) {
+ n, err := r.Reader.Read(p)
+ if n > 0 {
+ r.N += n
+ }
+ return n, err
+}
+
+func importImages(ctx context.Context, client *containerd.Client, in io.Reader, snapshotter string, platformMC platforms.MatchComparer) ([]images.Image, error) {
+ // In addition to passing WithImagePlatform() to client.Import(), we also need to pass WithDefaultPlatform() to NewClient().
+ // Otherwise unpacking may fail.
+ r := &readCounter{Reader: in}
+ imgs, err := client.Import(ctx, r,
+ containerd.WithDigestRef(archive.DigestTranslator(snapshotter)),
+ containerd.WithSkipDigestRef(func(name string) bool { return name != "" }),
+ containerd.WithImportPlatform(platformMC),
+ )
+ if err != nil {
+ if r.N == 0 {
+ // Avoid confusing "unrecognized image format"
+ return nil, errors.New("no image was built")
+ }
+ if errors.Is(err, images.ErrEmptyWalk) {
+ err = fmt.Errorf("%w (Hint: set `--platform=PLATFORM` or `--all-platforms`)", err)
+ }
+ return nil, err
+ }
+ return imgs, nil
+}
+
+func unpackImage(ctx context.Context, client *containerd.Client, model images.Image, platform platforms.MatchComparer, options types.ImageLoadOptions) error {
+ image := containerd.NewImageWithPlatform(client, model, platform)
+
+ if !options.Quiet {
+ fmt.Fprintf(options.Stdout, "unpacking %s (%s)...\n", model.Name, model.Target.Digest)
+ }
+
+ err := image.Unpack(ctx, options.GOptions.Snapshotter)
+ if err != nil {
+ return err
+ }
+
+ // Loaded message is shown even when quiet.
+ repo, tag := imgutil.ParseRepoTag(model.Name)
+ fmt.Fprintf(options.Stdout, "Loaded image: %s:%s\n", repo, tag)
+
+ return nil
+}
diff --git a/pkg/imgutil/pull/pull.go b/pkg/imgutil/pull/pull.go
index 11a257684c8..e677e9cd732 100644
--- a/pkg/imgutil/pull/pull.go
+++ b/pkg/imgutil/pull/pull.go
@@ -21,13 +21,15 @@ import (
"context"
"io"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/log"
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/nerdctl/pkg/imgutil/jobs"
- "github.com/containerd/nerdctl/pkg/platformutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
)
// Config for content fetch
diff --git a/pkg/imgutil/push/push.go b/pkg/imgutil/push/push.go
index d16b4b613f7..94c6acca71c 100644
--- a/pkg/imgutil/push/push.go
+++ b/pkg/imgutil/push/push.go
@@ -25,22 +25,22 @@ import (
"text/tabwriter"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/log"
- "github.com/containerd/containerd/pkg/progress"
- "github.com/containerd/containerd/platforms"
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/containerd/remotes/docker"
- "github.com/containerd/nerdctl/pkg/imgutil/dockerconfigresolver"
- "github.com/containerd/nerdctl/pkg/imgutil/jobs"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
-
"golang.org/x/sync/errgroup"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/containerd/v2/core/remotes/docker"
+ "github.com/containerd/containerd/v2/pkg/progress"
+ "github.com/containerd/log"
+ "github.com/containerd/platforms"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs"
)
// Push pushes an image to a remote registry.
-func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, stdout io.Writer,
+func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resolver, pushTracker docker.StatusTracker, stdout io.Writer,
localRef, remoteRef string, platform platforms.MatchComparer, allowNonDist, quiet bool) error {
img, err := client.ImageService().Get(ctx, localRef)
if err != nil {
@@ -48,7 +48,7 @@ func Push(ctx context.Context, client *containerd.Client, resolver remotes.Resol
}
desc := img.Target
- ongoing := newPushJobs(dockerconfigresolver.PushTracker)
+ ongoing := newPushJobs(pushTracker)
eg, ctx := errgroup.WithContext(ctx)
diff --git a/pkg/imgutil/snapshotter.go b/pkg/imgutil/snapshotter.go
index 0655c4774ce..15f63e7c00e 100644
--- a/pkg/imgutil/snapshotter.go
+++ b/pkg/imgutil/snapshotter.go
@@ -19,18 +19,23 @@ package imgutil
import (
"strings"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- ctdsnapshotters "github.com/containerd/containerd/pkg/snapshotters"
- "github.com/containerd/nerdctl/pkg/imgutil/pull"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ ctdsnapshotters "github.com/containerd/containerd/v2/pkg/snapshotters"
+ "github.com/containerd/log"
"github.com/containerd/stargz-snapshotter/fs/source"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/pull"
+ "github.com/containerd/nerdctl/v2/pkg/snapshotterutil"
)
const (
snapshotterNameOverlaybd = "overlaybd"
snapshotterNameStargz = "stargz"
snapshotterNameNydus = "nydus"
+ snapshotterNameSoci = "soci"
+ snapshotterNameCvmfs = "cvmfs-snapshotter"
// prefetch size for stargz
prefetchSize = 10 * 1024 * 1024
@@ -41,12 +46,14 @@ var builtinRemoteSnapshotterOpts = map[string]snapshotterOpts{
snapshotterNameOverlaybd: &remoteSnapshotterOpts{snapshotter: "overlaybd"},
snapshotterNameStargz: &remoteSnapshotterOpts{snapshotter: "stargz", extraLabels: stargzExtraLabels},
snapshotterNameNydus: &remoteSnapshotterOpts{snapshotter: "nydus"},
+ snapshotterNameSoci: &remoteSnapshotterOpts{snapshotter: "soci", extraLabels: sociExtraLabels},
+ snapshotterNameCvmfs: &remoteSnapshotterOpts{snapshotter: "cvmfs-snapshotter"},
}
// snapshotterOpts is used to update pull config
// for different snapshotters
type snapshotterOpts interface {
- apply(config *pull.Config, ref string)
+ apply(config *pull.Config, ref string, rFlags types.RemoteSnapshotterFlags)
isRemote() bool
}
@@ -55,7 +62,7 @@ func getSnapshotterOpts(snapshotter string) snapshotterOpts {
for sn, sno := range builtinRemoteSnapshotterOpts {
if strings.Contains(snapshotter, sn) {
if snapshotter != sn {
- logrus.Debugf("assuming %s to be a %s-compatible snapshotter", snapshotter, sn)
+ log.L.Debugf("assuming %s to be a %s-compatible snapshotter", snapshotter, sn)
}
return sno
}
@@ -68,17 +75,17 @@ func getSnapshotterOpts(snapshotter string) snapshotterOpts {
// interface `snapshotterOpts.isRemote()` function
type remoteSnapshotterOpts struct {
snapshotter string
- extraLabels func(func(images.Handler) images.Handler) func(images.Handler) images.Handler
+ extraLabels func(func(images.Handler) images.Handler, types.RemoteSnapshotterFlags) func(images.Handler) images.Handler
}
func (rs *remoteSnapshotterOpts) isRemote() bool {
return true
}
-func (rs *remoteSnapshotterOpts) apply(config *pull.Config, ref string) {
+func (rs *remoteSnapshotterOpts) apply(config *pull.Config, ref string, rFlags types.RemoteSnapshotterFlags) {
h := ctdsnapshotters.AppendInfoHandlerWrapper(ref)
if rs.extraLabels != nil {
- h = rs.extraLabels(h)
+ h = rs.extraLabels(h, rFlags)
}
config.RemoteOpts = append(
config.RemoteOpts,
@@ -93,7 +100,7 @@ type defaultSnapshotterOpts struct {
snapshotter string
}
-func (dsn *defaultSnapshotterOpts) apply(config *pull.Config, _ref string) {
+func (dsn *defaultSnapshotterOpts) apply(config *pull.Config, _ref string, rFlags types.RemoteSnapshotterFlags) {
config.RemoteOpts = append(
config.RemoteOpts,
containerd.WithPullSnapshotter(dsn.snapshotter))
@@ -104,6 +111,10 @@ func (dsn *defaultSnapshotterOpts) isRemote() bool {
return false
}
-func stargzExtraLabels(f func(images.Handler) images.Handler) func(images.Handler) images.Handler {
+func stargzExtraLabels(f func(images.Handler) images.Handler, rFlags types.RemoteSnapshotterFlags) func(images.Handler) images.Handler {
return source.AppendExtraLabelsHandler(prefetchSize, f)
}
+
+func sociExtraLabels(f func(images.Handler) images.Handler, rFlags types.RemoteSnapshotterFlags) func(images.Handler) images.Handler {
+ return snapshotterutil.SociAppendDefaultLabelsHandlerWrapper(rFlags.SociIndexDigest, f)
+}
diff --git a/pkg/imgutil/snapshotter_test.go b/pkg/imgutil/snapshotter_test.go
index 975d421f811..6acc6951ed3 100644
--- a/pkg/imgutil/snapshotter_test.go
+++ b/pkg/imgutil/snapshotter_test.go
@@ -21,12 +21,15 @@ import (
"reflect"
"testing"
- "github.com/containerd/containerd"
- ctdsnapshotters "github.com/containerd/containerd/pkg/snapshotters"
- "github.com/containerd/nerdctl/pkg/imgutil/pull"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/v3/assert"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ ctdsnapshotters "github.com/containerd/containerd/v2/pkg/snapshotters"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil/pull"
)
const (
@@ -52,6 +55,10 @@ func TestGetSnapshotterOpts(t *testing.T) {
sns: []string{"stargz", "stargz-v1"},
check: remoteSnOpts("stargz", true),
},
+ {
+ sns: []string{"soci"},
+ check: remoteSnOpts("soci", true),
+ },
{
sns: []string{"overlaybd", "overlaybd-v2"},
check: sameOpts(&remoteSnapshotterOpts{snapshotter: "overlaybd"}),
@@ -89,7 +96,8 @@ func sameOpts(want snapshotterOpts) func(*testing.T, snapshotterOpts) {
func getAndApplyRemoteOpts(t *testing.T, sn string) *containerd.RemoteContext {
config := &pull.Config{}
snOpts := getSnapshotterOpts(sn)
- snOpts.apply(config, testRef)
+ rFlags := types.RemoteSnapshotterFlags{}
+ snOpts.apply(config, testRef, rFlags)
rc := &containerd.RemoteContext{}
for _, o := range config.RemoteOpts {
@@ -132,6 +140,12 @@ func TestRemoteSnapshotterOpts(t *testing.T) {
checkRemoteSnapshotterAnnotataions, checkStargzSnapshotterAnnotataions,
},
},
+ {
+ name: "soci",
+ check: []func(t *testing.T, a map[string]string){
+ checkRemoteSnapshotterAnnotataions, checkSociSnapshotterAnnotataions,
+ },
+ },
{
name: "nydus",
check: []func(t *testing.T, a map[string]string){checkRemoteSnapshotterAnnotataions},
@@ -175,3 +189,16 @@ func checkStargzSnapshotterAnnotataions(t *testing.T, a map[string]string) {
_, ok := a["containerd.io/snapshot/remote/urls"]
assert.Equal(t, ok, true)
}
+
+// using values from soci source to check for annotations (
+// see https://github.com/awslabs/soci-snapshotter/blob/b05ba712d246ecc5146469f87e5e9305702fd72b/fs/source/source.go#L80C1-L80C6
+func checkSociSnapshotterAnnotataions(t *testing.T, a map[string]string) {
+ assert.Check(t, a != nil)
+ _, ok := a["containerd.io/snapshot/remote/soci.size"]
+ assert.Equal(t, ok, true)
+ _, ok = a["containerd.io/snapshot/remote/image.layers.size"]
+ assert.Equal(t, ok, true)
+ _, ok = a["containerd.io/snapshot/remote/soci.index.digest"]
+ assert.Equal(t, ok, true)
+
+}
diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go
index c3eec405948..43041dd5d1b 100644
--- a/pkg/infoutil/infoutil.go
+++ b/pkg/infoutil/infoutil.go
@@ -26,24 +26,25 @@ import (
"time"
"github.com/Masterminds/semver/v3"
- "github.com/containerd/containerd"
- ptypes "github.com/containerd/containerd/protobuf/types"
- "github.com/containerd/containerd/services/introspection"
- "github.com/containerd/nerdctl/pkg/buildkitutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/logging"
- "github.com/containerd/nerdctl/pkg/version"
- "github.com/sirupsen/logrus"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/introspection"
+ ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/buildkitutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/version"
)
func NativeDaemonInfo(ctx context.Context, client *containerd.Client) (*native.DaemonInfo, error) {
introService := client.IntrospectionService()
- plugins, err := introService.Plugins(ctx, nil)
+ plugins, err := introService.Plugins(ctx)
if err != nil {
return nil, err
}
- server, err := introService.Server(ctx, &ptypes.Empty{})
+ server, err := introService.Server(ctx)
if err != nil {
return nil, err
}
@@ -67,7 +68,7 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan
return nil, err
}
introService := client.IntrospectionService()
- daemonIntro, err := introService.Server(ctx, &ptypes.Empty{})
+ daemonIntro, err := introService.Server(ctx)
if err != nil {
return nil, err
}
@@ -80,7 +81,6 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan
info.ID = daemonIntro.UUID
// Storage drivers and logging drivers are not really Server concept for nerdctl, but mimics `docker info` output
info.Driver = snapshotter
- info.Plugins.Log = logging.Drivers()
info.Plugins.Storage = snapshotterPlugins
info.SystemTime = time.Now().Format(time.RFC3339Nano)
info.LoggingDriver = "json-file" // hard-coded
@@ -101,7 +101,7 @@ func Info(ctx context.Context, client *containerd.Client, snapshotter, cgroupMan
func GetSnapshotterNames(ctx context.Context, introService introspection.Service) ([]string, error) {
var names []string
- plugins, err := introService.Plugins(ctx, nil)
+ plugins, err := introService.Plugins(ctx)
if err != nil {
return nil, err
}
@@ -115,8 +115,8 @@ func GetSnapshotterNames(ctx context.Context, introService introspection.Service
func ClientVersion() dockercompat.ClientVersion {
return dockercompat.ClientVersion{
- Version: version.Version,
- GitCommit: version.Revision,
+ Version: version.GetVersion(),
+ GitCommit: version.GetRevision(),
GoVersion: runtime.Version(),
Os: runtime.GOOS,
Arch: runtime.GOARCH,
@@ -160,19 +160,19 @@ func ServerSemVer(ctx context.Context, client *containerd.Client) (*semver.Versi
func buildctlVersion() dockercompat.ComponentVersion {
buildctlBinary, err := buildkitutil.BuildctlBinary()
if err != nil {
- logrus.Warnf("unable to determine buildctl version: %s", err.Error())
+ log.L.Warnf("unable to determine buildctl version: %s", err.Error())
return dockercompat.ComponentVersion{Name: "buildctl"}
}
stdout, err := exec.Command(buildctlBinary, "--version").Output()
if err != nil {
- logrus.Warnf("unable to determine buildctl version: %s", err.Error())
+ log.L.Warnf("unable to determine buildctl version: %s", err.Error())
return dockercompat.ComponentVersion{Name: "buildctl"}
}
v, err := parseBuildctlVersion(stdout)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
return dockercompat.ComponentVersion{Name: "buildctl"}
}
return *v
@@ -205,12 +205,12 @@ func parseBuildctlVersion(buildctlVersionStdout []byte) (*dockercompat.Component
func runcVersion() dockercompat.ComponentVersion {
stdout, err := exec.Command("runc", "--version").Output()
if err != nil {
- logrus.Warnf("unable to determine runc version: %s", err.Error())
+ log.L.Warnf("unable to determine runc version: %s", err.Error())
return dockercompat.ComponentVersion{Name: "runc"}
}
v, err := parseRuncVersion(stdout)
if err != nil {
- logrus.Warn(err)
+ log.L.Warn(err)
return dockercompat.ComponentVersion{Name: "runc"}
}
return *v
@@ -228,7 +228,7 @@ func parseRuncVersion(runcVersionStdout []byte) (*dockercompat.ComponentVersion,
for _, detailsLine := range versionList[1:] {
detail := strings.SplitN(detailsLine, ":", 2)
if len(detail) != 2 {
- logrus.Warnf("unable to determine one of runc details, got: %s, %d", detail, len(detail))
+ log.L.Warnf("unable to determine one of runc details, got: %s, %d", detail, len(detail))
continue
}
switch strings.TrimSpace(detail[0]) {
diff --git a/pkg/infoutil/infoutil_freebsd.go b/pkg/infoutil/infoutil_freebsd.go
index ca8d67c6eaa..b47c63df524 100644
--- a/pkg/infoutil/infoutil_freebsd.go
+++ b/pkg/infoutil/infoutil_freebsd.go
@@ -17,8 +17,8 @@
package infoutil
import (
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/docker/docker/pkg/sysinfo"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/sysinfo"
)
const UnameO = "FreeBSD"
diff --git a/pkg/infoutil/infoutil_linux.go b/pkg/infoutil/infoutil_linux.go
index f7f7ef0d81b..93d22ee941c 100644
--- a/pkg/infoutil/infoutil_linux.go
+++ b/pkg/infoutil/infoutil_linux.go
@@ -20,13 +20,15 @@ import (
"fmt"
"strings"
- "github.com/containerd/cgroups/v3"
- "github.com/containerd/nerdctl/pkg/apparmorutil"
- "github.com/containerd/nerdctl/pkg/defaults"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/containerd/nerdctl/pkg/rootlessutil"
"github.com/docker/docker/pkg/meminfo"
- "github.com/docker/docker/pkg/sysinfo"
+
+ "github.com/containerd/cgroups/v3"
+
+ "github.com/containerd/nerdctl/v2/pkg/apparmorutil"
+ "github.com/containerd/nerdctl/v2/pkg/defaults"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/rootlessutil"
+ "github.com/containerd/nerdctl/v2/pkg/sysinfo"
)
const UnameO = "GNU/Linux"
@@ -49,7 +51,7 @@ WARNING: AppArmor profile %q is not loaded.
This warning is negligible if you do not intend to use AppArmor.`), defaults.AppArmorProfileName))
}
}
- info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile=default")
+ info.SecurityOptions = append(info.SecurityOptions, "name=seccomp,profile="+defaults.SeccompProfileName)
if defaults.CgroupnsMode() == "private" {
info.SecurityOptions = append(info.SecurityOptions, "name=cgroupns")
}
diff --git a/pkg/infoutil/infoutil_test.go b/pkg/infoutil/infoutil_test.go
index 632e9dd03c6..33cbe6cc0b4 100644
--- a/pkg/infoutil/infoutil_test.go
+++ b/pkg/infoutil/infoutil_test.go
@@ -19,8 +19,9 @@ package infoutil
import (
"testing"
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
"gotest.tools/v3/assert"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
)
func TestParseBuildctlVersion(t *testing.T) {
diff --git a/pkg/infoutil/infoutil_unix.go b/pkg/infoutil/infoutil_unix.go
index e49f27e5e05..1a4068a067a 100644
--- a/pkg/infoutil/infoutil_unix.go
+++ b/pkg/infoutil/infoutil_unix.go
@@ -1,4 +1,4 @@
-//go:build freebsd || linux
+//go:build unix
/*
Copyright The containerd Authors.
@@ -23,7 +23,6 @@ import (
"io"
"os"
"regexp"
-
"strings"
"golang.org/x/sys/unix"
diff --git a/pkg/infoutil/infoutil_unix_test.go b/pkg/infoutil/infoutil_unix_test.go
index 47067a8d1fa..208aedbafd3 100644
--- a/pkg/infoutil/infoutil_unix_test.go
+++ b/pkg/infoutil/infoutil_unix_test.go
@@ -1,4 +1,4 @@
-//go:build freebsd || linux
+//go:build unix
/*
Copyright The containerd Authors.
diff --git a/pkg/infoutil/infoutil_windows.go b/pkg/infoutil/infoutil_windows.go
index 3131731eb3a..a8075078605 100644
--- a/pkg/infoutil/infoutil_windows.go
+++ b/pkg/infoutil/infoutil_windows.go
@@ -17,33 +17,211 @@
package infoutil
import (
- "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat"
- "github.com/docker/docker/pkg/sysinfo"
+ "fmt"
+ "runtime"
+ "strings"
+
+ "github.com/docker/docker/pkg/meminfo"
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
+ "github.com/containerd/nerdctl/v2/pkg/sysinfo"
+)
+
+const UnameO = "Microsoft Windows"
+
+// MsiNTProductType is the product type of the operating system.
+// https://learn.microsoft.com/en-us/windows/win32/msi/msintproducttype
+// Ref: https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-osversioninfoexa
+const (
+ verNTServer = 0x0000003
)
-// UnameR returns `uname -r`
+type windowsInfoUtil interface {
+ RtlGetVersion() *windows.OsVersionInfoEx
+ GetRegistryStringValue(key registry.Key, path string, name string) (string, error)
+ GetRegistryIntValue(key registry.Key, path string, name string) (int, error)
+}
+
+type winInfoUtil struct{}
+
+// RtlGetVersion implements the RtlGetVersion method using the actual windows package
+func (sw *winInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx {
+ return windows.RtlGetVersion()
+}
+
+// UnameR returns the Kernel version
func UnameR() string {
- return ""
+ util := &winInfoUtil{}
+ version, err := getKernelVersion(util)
+ if err != nil {
+ log.L.Error(err.Error())
+ }
+
+ return version
}
-// UnameM returns `uname -m`
+// UnameM returns the architecture of the system
func UnameM() string {
- return ""
+ arch := runtime.GOARCH
+
+ if strings.ToLower(arch) == "amd64" {
+ return "x86_64"
+ }
+
+ // "386": 32-bit Intel/AMD processors (x86 architecture)
+ if strings.ToLower(arch) == "386" {
+ return "x86"
+ }
+
+ // arm, s390x, and so on
+ return arch
}
+// DistroName returns version information about the currently running operating system
func DistroName() string {
- return ""
+ util := &winInfoUtil{}
+ version, err := distroName(util)
+ if err != nil {
+ log.L.Error(err.Error())
+ }
+
+ return version
+}
+
+func distroName(sw windowsInfoUtil) (string, error) {
+ // Get the OS version information from the Windows registry
+ regPath := `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
+
+ // Eg. 22631 (REG_SZ)
+ currBuildNo, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, regPath, "CurrentBuildNumber")
+ if err != nil {
+ return "", fmt.Errorf("failed to get os version (build number) %v", err)
+ }
+
+ // Eg. 23H2 (REG_SZ)
+ displayVersion, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, regPath, "DisplayVersion")
+ if err != nil {
+ return "", fmt.Errorf("failed to get os version (display version) %v", err)
+ }
+
+ // UBR: Update Build Revision. Eg. 3737 (REG_DWORD 32-bit Value)
+ ubr, err := sw.GetRegistryIntValue(registry.LOCAL_MACHINE, regPath, "UBR")
+ if err != nil {
+ return "", fmt.Errorf("failed to get os version (ubr) %v", err)
+ }
+
+ productType := ""
+ if isWindowsServer(sw) {
+ productType = "Server"
+ }
+
+ // Concatenate the reg.key values to get the OS version information
+ // Example: "Microsoft Windows Version 23H2 (OS Build 22631.3737)"
+ versionString := fmt.Sprintf("%s %s Version %s (OS Build %s.%d)",
+ UnameO,
+ productType,
+ displayVersion,
+ currBuildNo,
+ ubr,
+ )
+
+ // Replace double spaces with single spaces
+ versionString = strings.ReplaceAll(versionString, " ", " ")
+
+ return versionString, nil
+}
+
+func getKernelVersion(sw windowsInfoUtil) (string, error) {
+ // Get BuildLabEx value from the Windows registry
+ // [buiild number].[revision number].[architecture].[branch].[date]-[time]
+ // Eg. "BuildLabEx: 10240.16412.amd64fre.th1.150729-1800"
+ buildLab, err := sw.GetRegistryStringValue(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, "BuildLabEx")
+ if err != nil {
+ return "", err
+ }
+
+ // Get Version: Contains the major and minor version numbers of the operating system.
+ // Eg. "10.0"
+ osvi := sw.RtlGetVersion()
+
+ // Concatenate the OS version and BuildLabEx values to get the Kernel version information
+ // Example: "10.0 22631 (10240.16412.amd64fre.th1.150729-1800)"
+ version := fmt.Sprintf("%d.%d %d (%s)", osvi.MajorVersion, osvi.MinorVersion, osvi.BuildNumber, buildLab)
+ return version, nil
}
+// GetRegistryStringValue retrieves a string value from the Windows registry
+func (sw *winInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) {
+ k, err := registry.OpenKey(key, path, registry.QUERY_VALUE)
+ if err != nil {
+ return "", err
+ }
+ defer k.Close()
+
+ v, _, err := k.GetStringValue(name)
+ if err != nil {
+ return "", err
+ }
+ return v, nil
+}
+
+// GetRegistryIntValue retrieves an integer value from the Windows registry
+func (sw *winInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) {
+ k, err := registry.OpenKey(key, path, registry.QUERY_VALUE)
+ if err != nil {
+ return 0, err
+ }
+ defer k.Close()
+
+ v, _, err := k.GetIntegerValue(name)
+ if err != nil {
+ return 0, err
+ }
+ return int(v), nil
+}
+
+func isWindowsServer(sw windowsInfoUtil) bool {
+ osvi := sw.RtlGetVersion()
+ return osvi.ProductType == verNTServer
+}
+
+// Cgroups not supported on Windows
func CgroupsVersion() string {
return ""
}
func fulfillPlatformInfo(info *dockercompat.Info) {
- // unimplemented
+ mobySysInfo := mobySysInfo(info)
+
+ // NOTE: cgroup fields are not available on Windows
+ // https://techcommunity.microsoft.com/t5/containers/introducing-the-host-compute-service-hcs/ba-p/382332
+
+ info.IPv4Forwarding = !mobySysInfo.IPv4ForwardingDisabled
+ if !info.IPv4Forwarding {
+ info.Warnings = append(info.Warnings, "WARNING: IPv4 forwarding is disabled")
+ }
+ info.BridgeNfIptables = !mobySysInfo.BridgeNFCallIPTablesDisabled
+ if !info.BridgeNfIptables {
+ info.Warnings = append(info.Warnings, "WARNING: bridge-nf-call-iptables is disabled")
+ }
+ info.BridgeNfIP6tables = !mobySysInfo.BridgeNFCallIP6TablesDisabled
+ if !info.BridgeNfIP6tables {
+ info.Warnings = append(info.Warnings, "WARNING: bridge-nf-call-ip6tables is disabled")
+ }
+ info.NCPU = sysinfo.NumCPU()
+ memLimit, err := meminfo.Read()
+ if err != nil {
+ info.Warnings = append(info.Warnings, fmt.Sprintf("failed to read mem info: %v", err))
+ } else {
+ info.MemTotal = memLimit.MemTotal
+ }
}
-func mobySysInfo(info *dockercompat.Info) *sysinfo.SysInfo {
+func mobySysInfo(_ *dockercompat.Info) *sysinfo.SysInfo {
var sysinfo sysinfo.SysInfo
return &sysinfo
}
diff --git a/pkg/infoutil/infoutil_windows_test.go b/pkg/infoutil/infoutil_windows_test.go
new file mode 100644
index 00000000000..173cf1927e4
--- /dev/null
+++ b/pkg/infoutil/infoutil_windows_test.go
@@ -0,0 +1,201 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package infoutil
+
+import (
+ "testing"
+
+ "go.uber.org/mock/gomock"
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
+ "gotest.tools/v3/assert"
+
+ mocks "github.com/containerd/nerdctl/v2/pkg/infoutil/infoutilmock"
+)
+
+func setUpMocks(t *testing.T) *mocks.MockWindowsInfoUtil {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl)
+
+ // Mock registry value: CurrentBuildNumber
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryStringValue(gomock.Any(), gomock.Any(), "CurrentBuildNumber").
+ Return("19041", nil).
+ AnyTimes()
+
+ // Mock registry value: DisplayVersion
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryStringValue(gomock.Any(), gomock.Any(), "DisplayVersion").
+ Return("22H4", nil).
+ AnyTimes()
+
+ // Mock registry value: UBR
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryIntValue(gomock.Any(), gomock.Any(), "UBR").
+ Return(558, nil).
+ AnyTimes()
+
+ return mockInfoUtil
+}
+
+const (
+ verNTWorkStation = 0x0000001
+ verNTDomainController = 0x0000002
+)
+
+func TestDistroName(t *testing.T) {
+ mockInfoUtil := setUpMocks(t)
+
+ baseVersion := windows.OsVersionInfoEx{
+ MajorVersion: 10,
+ MinorVersion: 0,
+ BuildNumber: 19041,
+ }
+
+ tests := []struct {
+ productType byte
+ expected string
+ }{
+ {
+ productType: verNTWorkStation,
+ expected: "Microsoft Windows Version 22H4 (OS Build 19041.558)",
+ },
+ {
+ productType: verNTServer,
+ expected: "Microsoft Windows Server Version 22H4 (OS Build 19041.558)",
+ },
+ }
+
+ for _, tt := range tests {
+ // Mock sys/windows RtlGetVersion
+ osvi := baseVersion
+ osvi.ProductType = tt.productType
+ mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1)
+
+ t.Run(tt.expected, func(t *testing.T) {
+ actual, err := distroName(mockInfoUtil)
+ assert.Equal(t, tt.expected, actual, "distroName should return the name of the operating system")
+ assert.NilError(t, err)
+ })
+ }
+}
+
+func TestDistroNameError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl)
+
+ mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0)
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return("19041", registry.ErrNotExist).AnyTimes()
+
+ actual, err := distroName(mockInfoUtil)
+ assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "distroName should return an error on error")
+ assert.Equal(t, "", actual, "distroname should return an empty string on error")
+}
+
+func TestGetKernelVersion(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl)
+
+ // Mock registry value: BuildLabEx
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryStringValue(gomock.Any(), gomock.Any(), "BuildLabEx").
+ Return("10240.16412.amd64fre.th1.150729-1800", nil).
+ Times(1)
+
+ baseVersion := windows.OsVersionInfoEx{
+ MajorVersion: 10,
+ MinorVersion: 0,
+ BuildNumber: 19041,
+ }
+
+ expected := "10.0 19041 (10240.16412.amd64fre.th1.150729-1800)"
+
+ // Mock sys/windows RtlGetVersion
+ osvi := baseVersion
+ mockInfoUtil.EXPECT().RtlGetVersion().Return(&osvi).Times(1)
+
+ actual, err := getKernelVersion(mockInfoUtil)
+ assert.NilError(t, err)
+ assert.Equal(t, expected, actual, "getKernelVersion should return the kernel version")
+}
+
+func TestGetKernelVersionError(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ mockInfoUtil := mocks.NewMockWindowsInfoUtil(ctrl)
+
+ mockInfoUtil.EXPECT().RtlGetVersion().Return(nil).Times(0)
+ mockInfoUtil.
+ EXPECT().
+ GetRegistryStringValue(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return("", registry.ErrNotExist).Times(1)
+
+ actual, err := getKernelVersion(mockInfoUtil)
+ assert.ErrorContains(t, err, registry.ErrNotExist.Error(), "getKernelVersion should return an error on error")
+ assert.Equal(t, "", actual, "getKernelVersion should return an empty string on error")
+}
+
+func TestIsWindowsServer(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ tests := []struct {
+ productType string
+ osvi windows.OsVersionInfoEx
+ expected bool
+ }{
+ {
+ productType: "VER_NT_WORKSTATION",
+ osvi: windows.OsVersionInfoEx{ProductType: verNTWorkStation},
+ expected: false,
+ },
+ {
+ productType: "VER_NT_DOMAIN_CONTROLLER",
+ osvi: windows.OsVersionInfoEx{ProductType: verNTDomainController},
+ expected: false,
+ },
+ {
+ productType: "VER_NT_SERVER",
+ osvi: windows.OsVersionInfoEx{ProductType: verNTServer},
+ expected: true,
+ },
+ }
+
+ mockSysCall := mocks.NewMockWindowsInfoUtil(ctrl)
+ for _, tt := range tests {
+ mockSysCall.EXPECT().RtlGetVersion().Return(&tt.osvi)
+
+ t.Run(tt.productType, func(t *testing.T) {
+ actual := isWindowsServer(mockSysCall)
+ assert.Equal(t, tt.expected, actual, "isWindowsServer should return true on Windows Server")
+ })
+ }
+}
diff --git a/pkg/infoutil/infoutilmock/info.util.mock.go b/pkg/infoutil/infoutilmock/info.util.mock.go
new file mode 100644
index 00000000000..298597ece67
--- /dev/null
+++ b/pkg/infoutil/infoutilmock/info.util.mock.go
@@ -0,0 +1,108 @@
+//go:build windows
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package infoutilmock
+
+import (
+ "reflect"
+
+ "go.uber.org/mock/gomock"
+ "golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
+)
+
+// MockWindowsInfoUtil is a mock of windowsInfoUtil interface
+type MockWindowsInfoUtil struct {
+ ctrl *gomock.Controller
+ recorder *MockWindowsInfoUtilMockRecorder
+}
+
+// MockWindowsInfoUtilMockRecorder is the mock recorder for MockWindowsInfoUtil
+type MockWindowsInfoUtilMockRecorder struct {
+ mock *MockWindowsInfoUtil
+}
+
+// NewMockWindowsInfoUtil creates a new mock instance
+func NewMockWindowsInfoUtil(ctrl *gomock.Controller) *MockWindowsInfoUtil {
+ mock := &MockWindowsInfoUtil{ctrl: ctrl}
+ mock.recorder = &MockWindowsInfoUtilMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockWindowsInfoUtil) EXPECT() *MockWindowsInfoUtilMockRecorder {
+ return m.recorder
+}
+
+// Create mocks the RtlGetVersion method of windowsInfoUtil
+func (m *MockWindowsInfoUtil) RtlGetVersion() *windows.OsVersionInfoEx {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "RtlGetVersion")
+ ret0, _ := ret[0].(*windows.OsVersionInfoEx)
+ return ret0
+}
+
+// Expected call of RtlGetVersion
+func (m *MockWindowsInfoUtilMockRecorder) RtlGetVersion() *gomock.Call {
+ m.mock.ctrl.T.Helper()
+ return m.mock.ctrl.RecordCallWithMethodType(
+ m.mock,
+ "RtlGetVersion",
+ reflect.TypeOf((*MockWindowsInfoUtil)(nil).RtlGetVersion),
+ )
+}
+
+// Create mocks the GetRegistryStringValue method of windowsInfoUtil
+func (m *MockWindowsInfoUtil) GetRegistryStringValue(key registry.Key, path string, name string) (string, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetRegistryStringValue", key, path, name)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Expected call of GetRegistryStringValue
+func (m *MockWindowsInfoUtilMockRecorder) GetRegistryStringValue(key any, path any, name any) *gomock.Call {
+ m.mock.ctrl.T.Helper()
+ return m.mock.ctrl.RecordCallWithMethodType(
+ m.mock,
+ "GetRegistryStringValue",
+ reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryStringValue),
+ key, path, name,
+ )
+}
+
+// Create mocks the GetRegistryIntValue method of windowsInfoUtil
+func (m *MockWindowsInfoUtil) GetRegistryIntValue(key registry.Key, path string, name string) (int, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetRegistryIntValue", key, path, name)
+ ret0, _ := ret[0].(int)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// Expected call of GetRegistryIntValue
+func (m *MockWindowsInfoUtilMockRecorder) GetRegistryIntValue(key any, path any, name any) *gomock.Call {
+ m.mock.ctrl.T.Helper()
+ return m.mock.ctrl.RecordCallWithMethodType(
+ m.mock,
+ "GetRegistryIntValue",
+ reflect.TypeOf((*MockWindowsInfoUtil)(nil).GetRegistryIntValue),
+ key, path, name,
+ )
+}
diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go
index 843c59c705c..aceec3bca3d 100644
--- a/pkg/inspecttypes/dockercompat/dockercompat.go
+++ b/pkg/inspecttypes/dockercompat/dockercompat.go
@@ -35,40 +35,53 @@ import (
"strings"
"time"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/runtime/restart"
- gocni "github.com/containerd/go-cni"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/inspecttypes/native"
- "github.com/containerd/nerdctl/pkg/labels"
"github.com/docker/go-connections/nat"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
- "github.com/tidwall/gjson"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/runtime/restart"
+ "github.com/containerd/go-cni"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+ "github.com/containerd/nerdctl/v2/pkg/ocihook/state"
)
-// Image mimics a `docker image inspect` object.
-// From https://github.com/moby/moby/blob/v20.10.1/api/types/types.go#L340-L374
+// From https://github.com/moby/moby/blob/v26.1.2/api/types/types.go#L34-L140
type Image struct {
- ID string `json:"Id"`
- RepoTags []string
- RepoDigests []string
- // TODO: Parent string
- Comment string
- Created string
- // TODO: Container string
- // TODO: ContainerConfig *container.Config
- // TODO: DockerVersion string
- Author string
- Config *Config
- Architecture string
- // TODO: Variant string `json:",omitempty"`
- Os string
+ ID string `json:"Id"`
+ RepoTags []string
+ RepoDigests []string
+ Parent string
+ Comment string
+ Created string
+ DockerVersion string
+ Author string
+ Config *Config
+ Architecture string
+ Variant string `json:",omitempty"`
+ Os string
+
// TODO: OsVersion string `json:",omitempty"`
- Size int64 // Size is the unpacked size of the image
- // TODO: GraphDriver GraphDriverData
+
+ Size int64 // Size is the unpacked size of the image
+ VirtualSize int64 `json:"VirtualSize,omitempty"` // Deprecated
+
+ // TODO: GraphDriver GraphDriverData
+
RootFS RootFS
Metadata ImageMetadata
+
+ // Deprecated: TODO: Container string
+ // Deprecated: TODO: ContainerConfig *container.Config
+}
+
+// From: https://github.com/moby/moby/blob/v26.1.2/api/types/graph_driver_data.go
+type GraphDriverData struct {
+ Data map[string]string `json:"Data"`
+ Name string `json:"Name"`
}
type RootFS struct {
@@ -105,8 +118,8 @@ type Container struct {
// TODO: ExecIDs []string
// TODO: HostConfig *container.HostConfig
// TODO: GraphDriver GraphDriverData
- // TODO: SizeRw *int64 `json:",omitempty"`
- // TODO: SizeRootFs *int64 `json:",omitempty"`
+ SizeRw *int64 `json:",omitempty"`
+ SizeRootFs *int64 `json:",omitempty"`
Mounts []MountPoint
Config *Config
@@ -164,16 +177,16 @@ type ContainerState struct {
Restarting bool
// TODO: OOMKilled bool
// TODO: Dead bool
- Pid int
- ExitCode int
- Error string
- // TODO: StartedAt string
+ Pid int
+ ExitCode int
+ Error string
+ StartedAt string
FinishedAt string
// TODO: Health *Health `json:",omitempty"`
}
type NetworkSettings struct {
- Ports *nat.PortMap `json:",omitempty"`
+ Ports *nat.PortMap
DefaultNetworkSettings
Networks map[string]*NetworkEndpointSettings
}
@@ -211,18 +224,22 @@ type NetworkEndpointSettings struct {
// ContainerFromNative instantiates a Docker-compatible Container from containerd-native Container.
func ContainerFromNative(n *native.Container) (*Container, error) {
+ var hostname string
c := &Container{
- ID: n.ID,
- Created: n.CreatedAt.Format(time.RFC3339Nano),
- Image: n.Image,
- Name: n.Labels[labels.Name],
- Driver: n.Snapshotter,
+ ID: n.ID,
+ Created: n.CreatedAt.Format(time.RFC3339Nano),
+ Image: n.Image,
+ Name: n.Labels[labels.Name],
+ Driver: n.Snapshotter,
+ // XXX is this always right? what if the container OS is NOT the same as the host OS?
Platform: runtime.GOOS, // for Docker compatibility, this Platform string does NOT contain arch like "/amd64"
}
if n.Labels[restart.StatusLabel] == string(containerd.Running) {
c.RestartCount, _ = strconv.Atoi(n.Labels[restart.CountLabel])
}
+ containerAnnotations := make(map[string]string)
if sp, ok := n.Spec.(*specs.Spec); ok {
+ containerAnnotations = sp.Annotations
if p := sp.Process; p != nil {
if len(p.Args) > 0 {
c.Path = p.Args[0]
@@ -232,15 +249,24 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
}
c.AppArmorProfile = p.ApparmorProfile
}
+ c.Mounts = mountsFromNative(sp.Mounts)
+ for _, mount := range c.Mounts {
+ if mount.Destination == "/etc/resolv.conf" {
+ c.ResolvConfPath = mount.Source
+ } else if mount.Destination == "/etc/hostname" {
+ c.HostnamePath = mount.Source
+ }
+ }
+ hostname = sp.Hostname
}
if nerdctlStateDir := n.Labels[labels.StateDir]; nerdctlStateDir != "" {
- c.ResolvConfPath = filepath.Join(nerdctlStateDir, "resolv.conf")
- if _, err := os.Stat(c.ResolvConfPath); err != nil {
- c.ResolvConfPath = ""
+ resolvConfPath := filepath.Join(nerdctlStateDir, "resolv.conf")
+ if _, err := os.Stat(resolvConfPath); err == nil {
+ c.ResolvConfPath = resolvConfPath
}
- c.HostnamePath = filepath.Join(nerdctlStateDir, "hostname")
- if _, err := os.Stat(c.HostnamePath); err != nil {
- c.HostnamePath = ""
+ hostnamePath := filepath.Join(nerdctlStateDir, "hostname")
+ if _, err := os.Stat(hostnamePath); err == nil {
+ c.HostnamePath = hostnamePath
}
c.LogPath = filepath.Join(nerdctlStateDir, n.ID+"-json.log")
if _, err := os.Stat(c.LogPath); err != nil {
@@ -265,7 +291,18 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
cs.Paused = n.Process.Status.Status == containerd.Paused
cs.Pid = n.Process.Pid
cs.ExitCode = int(n.Process.Status.ExitStatus)
- cs.FinishedAt = n.Process.Status.ExitTime.Format(time.RFC3339Nano)
+ if containerAnnotations[labels.StateDir] != "" {
+ if lf, err := state.New(containerAnnotations[labels.StateDir]); err != nil {
+ log.L.WithError(err).Errorf("failed retrieving state")
+ } else if err = lf.Load(); err != nil {
+ log.L.WithError(err).Errorf("failed retrieving StartedAt from state")
+ } else if !time.Time.IsZero(lf.StartedAt) {
+ cs.StartedAt = lf.StartedAt.UTC().Format(time.RFC3339Nano)
+ }
+ }
+ if !n.Process.Status.ExitTime.IsZero() {
+ cs.FinishedAt = n.Process.Status.ExitTime.Format(time.RFC3339Nano)
+ }
nSettings, err := networkSettingsFromNative(n.Process.NetNS, n.Spec.(*specs.Spec))
if err != nil {
return nil, err
@@ -274,56 +311,83 @@ func ContainerFromNative(n *native.Container) (*Container, error) {
}
c.State = cs
c.Config = &Config{
- Hostname: n.Labels[labels.Hostname],
- Labels: n.Labels,
+ Labels: n.Labels,
+ }
+ if n.Labels[labels.Hostname] != "" {
+ hostname = n.Labels[labels.Hostname]
}
+ c.Config.Hostname = hostname
return c, nil
}
-func ImageFromNative(n *native.Image) (*Image, error) {
- i := &Image{}
-
- imgoci := n.ImageConfig
+func ImageFromNative(nativeImage *native.Image) (*Image, error) {
+ imgOCI := nativeImage.ImageConfig
+ repository, tag := imgutil.ParseRepoTag(nativeImage.Image.Name)
+
+ image := &Image{
+ // Docker ID (digest of platform-specific config), not containerd ID (digest of multi-platform index or manifest)
+ ID: nativeImage.ImageConfigDesc.Digest.String(),
+ Parent: nativeImage.Image.Labels["org.mobyproject.image.parent"],
+ Architecture: imgOCI.Architecture,
+ Variant: imgOCI.Platform.Variant,
+ Os: imgOCI.OS,
+ Size: nativeImage.Size,
+ VirtualSize: nativeImage.Size,
+ RepoTags: []string{fmt.Sprintf("%s:%s", repository, tag)},
+ RepoDigests: []string{fmt.Sprintf("%s@%s", repository, nativeImage.Image.Target.Digest.String())},
+ }
- i.RootFS.Type = imgoci.RootFS.Type
- diffIDs := imgoci.RootFS.DiffIDs
- for _, d := range diffIDs {
- i.RootFS.Layers = append(i.RootFS.Layers, d.String())
+ if len(imgOCI.History) > 0 {
+ image.Comment = imgOCI.History[len(imgOCI.History)-1].Comment
+ image.Created = imgOCI.History[len(imgOCI.History)-1].Created.Format(time.RFC3339Nano)
+ image.Author = imgOCI.History[len(imgOCI.History)-1].Author
}
- if len(imgoci.History) > 0 {
- i.Comment = imgoci.History[len(imgoci.History)-1].Comment
- i.Created = imgoci.History[len(imgoci.History)-1].Created.Format(time.RFC3339Nano)
- i.Author = imgoci.History[len(imgoci.History)-1].Author
+
+ image.RootFS.Type = imgOCI.RootFS.Type
+ for _, d := range imgOCI.RootFS.DiffIDs {
+ image.RootFS.Layers = append(image.RootFS.Layers, d.String())
}
- i.Architecture = imgoci.Architecture
- i.Os = imgoci.OS
portSet := make(nat.PortSet)
- for k := range imgoci.Config.ExposedPorts {
+ for k := range imgOCI.Config.ExposedPorts {
portSet[nat.Port(k)] = struct{}{}
}
- i.Config = &Config{
- Cmd: imgoci.Config.Cmd,
- Volumes: imgoci.Config.Volumes,
- Env: imgoci.Config.Env,
- User: imgoci.Config.User,
- WorkingDir: imgoci.Config.WorkingDir,
- Entrypoint: imgoci.Config.Entrypoint,
- Labels: imgoci.Config.Labels,
+ image.Config = &Config{
+ Cmd: imgOCI.Config.Cmd,
+ Volumes: imgOCI.Config.Volumes,
+ Env: imgOCI.Config.Env,
+ User: imgOCI.Config.User,
+ WorkingDir: imgOCI.Config.WorkingDir,
+ Entrypoint: imgOCI.Config.Entrypoint,
+ Labels: imgOCI.Config.Labels,
ExposedPorts: portSet,
}
- i.ID = n.ImageConfigDesc.Digest.String() // Docker ID (digest of platform-specific config), not containerd ID (digest of multi-platform index or manifest)
+ return image, nil
+}
- repository, tag := imgutil.ParseRepoTag(n.Image.Name)
+// mountsFromNative only filters bind mount to transform from native container.
+// Because native container shows all types of mounts, such as tmpfs, proc, sysfs.
+func mountsFromNative(spMounts []specs.Mount) []MountPoint {
+ mountpoints := make([]MountPoint, 0, len(spMounts))
+ for _, m := range spMounts {
+ var mp MountPoint
+ if m.Type != "bind" {
+ continue
+ }
+ mp.Type = m.Type
+ mp.Source = m.Source
+ mp.Destination = m.Destination
+ mp.Mode = strings.Join(m.Options, ",")
+ mp.RW, mp.Propagation = ParseMountProperties(m.Options)
+ mountpoints = append(mountpoints, mp)
+ }
- i.RepoTags = []string{fmt.Sprintf("%s:%s", repository, tag)}
- i.RepoDigests = []string{fmt.Sprintf("%s@%s", repository, n.Image.Target.Digest.String())}
- i.Size = n.Size
- return i, nil
+ return mountpoints
}
+
func statusFromNative(x containerd.Status, labels map[string]string) string {
switch s := x.Status; s {
case containerd.Stopped:
@@ -337,12 +401,15 @@ func statusFromNative(x containerd.Status, labels map[string]string) string {
}
func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSettings, error) {
- if n == nil {
- return nil, nil
- }
res := &NetworkSettings{
Networks: make(map[string]*NetworkEndpointSettings),
}
+ resPortMap := make(nat.PortMap)
+ res.Ports = &resPortMap
+ if n == nil {
+ return res, nil
+ }
+
var primary *NetworkEndpointSettings
for _, x := range n.Interfaces {
if x.Interface.Flags&net.FlagLoopback != 0 {
@@ -357,7 +424,7 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting
for _, a := range x.Addrs {
ip, ipnet, err := net.ParseCIDR(a)
if err != nil {
- logrus.WithError(err).WithField("name", x.Name).Warnf("failed to parse %q", a)
+ log.L.WithError(err).WithField("name", x.Name).Warnf("failed to parse %q", a)
continue
}
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
@@ -377,7 +444,7 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting
res.Networks[fakeDockerNetworkName] = nes
if portsLabel, ok := sp.Annotations[labels.Ports]; ok {
- var ports []gocni.PortMapping
+ var ports []cni.PortMapping
err := json.Unmarshal([]byte(portsLabel), &ports)
if err != nil {
return nil, err
@@ -386,8 +453,11 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting
if err != nil {
return nil, err
}
- res.Ports = nports
+ for portLabel, portBindings := range *nports {
+ resPortMap[portLabel] = portBindings
+ }
}
+
if x.Index == n.PrimaryInterface {
primary = nes
}
@@ -403,7 +473,7 @@ func networkSettingsFromNative(n *native.NetNS, sp *specs.Spec) (*NetworkSetting
return res, nil
}
-func convertToNatPort(portMappings []gocni.PortMapping) (*nat.PortMap, error) {
+func convertToNatPort(portMappings []cni.PortMapping) (*nat.PortMap, error) {
portMap := make(nat.PortMap)
for _, portMapping := range portMappings {
ports := []nat.PortBinding{}
@@ -442,29 +512,29 @@ type Network struct {
// Scope, Driver, etc. are omitted
}
+type structuredCNI struct {
+ Name string `json:"name"`
+ Plugins []struct {
+ Ipam struct {
+ Ranges [][]IPAMConfig `json:"ranges"`
+ } `json:"ipam"`
+ } `json:"plugins"`
+}
+
func NetworkFromNative(n *native.Network) (*Network, error) {
var res Network
- nameResult := gjson.GetBytes(n.CNI, "name")
- if s, ok := nameResult.Value().(string); ok {
- res.Name = s
+ sCNI := &structuredCNI{}
+ err := json.Unmarshal(n.CNI, sCNI)
+ if err != nil {
+ return nil, err
}
- // flatten twice to get ipamRangesResult=[{ "subnet": "10.4.19.0/24", "gateway": "10.4.19.1" }]
- ipamRangesResult := gjson.GetBytes(n.CNI, "plugins.#.ipam.ranges|@flatten|@flatten")
- for _, f := range ipamRangesResult.Array() {
- m := f.Map()
- var cfg IPAMConfig
- if x, ok := m["subnet"]; ok {
- cfg.Subnet = x.String()
- }
- if x, ok := m["gateway"]; ok {
- cfg.Gateway = x.String()
- }
- if x, ok := m["ipRange"]; ok {
- cfg.IPRange = x.String()
+ res.Name = sCNI.Name
+ for _, plugin := range sCNI.Plugins {
+ for _, ranges := range plugin.Ipam.Ranges {
+ res.IPAM.Config = append(res.IPAM.Config, ranges...)
}
- res.IPAM.Config = append(res.IPAM.Config, cfg)
}
if n.NerdctlID != nil {
@@ -485,18 +555,12 @@ func parseMounts(nerdctlMounts string) ([]MountPoint, error) {
return nil, err
}
- for i := range mounts {
- rw, propagation := parseMountProperties(mounts[i].Mode)
- mounts[i].RW = rw
- mounts[i].Propagation = propagation
- }
-
return mounts, nil
}
-func parseMountProperties(option string) (rw bool, propagation string) {
+func ParseMountProperties(option []string) (rw bool, propagation string) {
rw = true
- for _, opt := range strings.Split(option, ",") {
+ for _, opt := range option {
switch opt {
case "ro", "rro":
rw = false
diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go
new file mode 100644
index 00000000000..12814bc31fc
--- /dev/null
+++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go
@@ -0,0 +1,363 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package dockercompat
+
+import (
+ "net"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/docker/go-connections/nat"
+ "github.com/opencontainers/runtime-spec/specs-go"
+ "gotest.tools/v3/assert"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+)
+
+func TestContainerFromNative(t *testing.T) {
+ tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644)
+ defer os.RemoveAll(tempStateDir)
+
+ testcase := []struct {
+ name string
+ n *native.Container
+ expected *Container
+ }{
+ // nerdctl container, mount /mnt/foo:/mnt/foo:rw,rslave; ResolvConfPath; hostname
+ {
+ name: "container from nerdctl",
+ n: &native.Container{
+ Container: containers.Container{
+ Labels: map[string]string{
+ "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]",
+ "nerdctl/state-dir": tempStateDir,
+ "nerdctl/hostname": "host1",
+ },
+ },
+ Spec: &specs.Spec{},
+ Process: &native.Process{
+ Pid: 10000,
+ Status: containerd.Status{
+ Status: "running",
+ },
+ },
+ },
+ expected: &Container{
+ Created: "0001-01-01T00:00:00Z",
+ Platform: runtime.GOOS,
+ ResolvConfPath: filepath.Join(tempStateDir, "resolv.conf"),
+ State: &ContainerState{
+ Status: "running",
+ Running: true,
+ Pid: 10000,
+ FinishedAt: "",
+ },
+ Mounts: []MountPoint{
+ {
+ Type: "bind",
+ Source: "/mnt/foo",
+ Destination: "/mnt/foo",
+ Mode: "rshared,rw",
+ RW: true,
+ Propagation: "rshared",
+ },
+ },
+ Config: &Config{
+ Labels: map[string]string{
+ "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]",
+ "nerdctl/state-dir": tempStateDir,
+ "nerdctl/hostname": "host1",
+ },
+ Hostname: "host1",
+ },
+ NetworkSettings: &NetworkSettings{
+ Ports: &nat.PortMap{},
+ Networks: map[string]*NetworkEndpointSettings{},
+ },
+ },
+ },
+ // cri container, mount /mnt/foo:/mnt/foo:rw,rslave; mount resolv.conf and hostname; internal sysfs mount
+ {
+ name: "container from cri",
+ n: &native.Container{
+ Container: containers.Container{},
+ Spec: &specs.Spec{
+ Mounts: []specs.Mount{
+ {
+ Destination: "/etc/resolv.conf",
+ Type: "bind",
+ Source: "/mock-sandbox-dir/resolv.conf",
+ Options: []string{"rbind", "rprivate", "rw"},
+ },
+ {
+ Destination: "/etc/hostname",
+ Type: "bind",
+ Source: "/mock-sandbox-dir/hostname",
+ Options: []string{"rbind", "rprivate", "rw"},
+ },
+ {
+ Destination: "/mnt/foo",
+ Type: "bind",
+ Source: "/mnt/foo",
+ Options: []string{"rbind", "rslave", "rw"},
+ },
+ {
+ Destination: "/sys",
+ Type: "sysfs",
+ Source: "sysfs",
+ Options: []string{"nosuid", "noexec", "nodev", "ro"},
+ },
+ },
+ },
+ Process: &native.Process{
+ Pid: 10000,
+ Status: containerd.Status{
+ Status: "running",
+ },
+ },
+ },
+ expected: &Container{
+ Created: "0001-01-01T00:00:00Z",
+ Platform: runtime.GOOS,
+ ResolvConfPath: "/mock-sandbox-dir/resolv.conf",
+ HostnamePath: "/mock-sandbox-dir/hostname",
+ State: &ContainerState{
+ Status: "running",
+ Running: true,
+ Pid: 10000,
+ FinishedAt: "",
+ },
+ Mounts: []MountPoint{
+ {
+ Type: "bind",
+ Source: "/mock-sandbox-dir/resolv.conf",
+ Destination: "/etc/resolv.conf",
+ Mode: "rbind,rprivate,rw",
+ RW: true,
+ Propagation: "rprivate",
+ },
+ {
+ Type: "bind",
+ Source: "/mock-sandbox-dir/hostname",
+ Destination: "/etc/hostname",
+ Mode: "rbind,rprivate,rw",
+ RW: true,
+ Propagation: "rprivate",
+ },
+ {
+ Type: "bind",
+ Source: "/mnt/foo",
+ Destination: "/mnt/foo",
+ Mode: "rbind,rslave,rw",
+ RW: true,
+ Propagation: "rslave",
+ },
+ // ignore sysfs mountpoint
+ },
+ Config: &Config{},
+ NetworkSettings: &NetworkSettings{
+ Ports: &nat.PortMap{},
+ Networks: map[string]*NetworkEndpointSettings{},
+ },
+ },
+ },
+ // ctr container, mount /mnt/foo:/mnt/foo:rw,rslave; internal sysfs mount; hostname
+ {
+ name: "container from ctr",
+ n: &native.Container{
+ Container: containers.Container{},
+ Spec: &specs.Spec{
+ Hostname: "host1",
+ Mounts: []specs.Mount{
+ {
+ Destination: "/mnt/foo",
+ Type: "bind",
+ Source: "/mnt/foo",
+ Options: []string{"rbind", "rslave", "rw"},
+ },
+ {
+ Destination: "/sys",
+ Type: "sysfs",
+ Source: "sysfs",
+ Options: []string{"nosuid", "noexec", "nodev", "ro"},
+ },
+ },
+ },
+ Process: &native.Process{
+ Pid: 10000,
+ Status: containerd.Status{
+ Status: "running",
+ },
+ },
+ },
+ expected: &Container{
+ Created: "0001-01-01T00:00:00Z",
+ Platform: runtime.GOOS,
+ State: &ContainerState{
+ Status: "running",
+ Running: true,
+ Pid: 10000,
+ FinishedAt: "",
+ },
+ Mounts: []MountPoint{
+ {
+ Type: "bind",
+ Source: "/mnt/foo",
+ Destination: "/mnt/foo",
+ Mode: "rbind,rslave,rw",
+ RW: true,
+ Propagation: "rslave",
+ },
+ // ignore sysfs mountpoint
+ },
+ Config: &Config{
+ Hostname: "host1",
+ },
+ NetworkSettings: &NetworkSettings{
+ Ports: &nat.PortMap{},
+ Networks: map[string]*NetworkEndpointSettings{},
+ },
+ },
+ },
+ }
+
+ for _, tc := range testcase {
+ t.Run(tc.name, func(tt *testing.T) {
+ d, _ := ContainerFromNative(tc.n)
+ assert.DeepEqual(tt, d, tc.expected)
+ })
+ }
+}
+
+func TestNetworkSettingsFromNative(t *testing.T) {
+ tempStateDir, err := os.MkdirTemp(t.TempDir(), "rw")
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.WriteFile(filepath.Join(tempStateDir, "resolv.conf"), []byte(""), 0644)
+ defer os.RemoveAll(tempStateDir)
+
+ testcase := []struct {
+ name string
+ n *native.NetNS
+ s *specs.Spec
+ expected *NetworkSettings
+ }{
+ // Given null native.NetNS, Return initialized NetworkSettings
+ // UseCase: Inspect a Stopped Container
+ {
+ name: "Given Null NetNS, Return initialized NetworkSettings",
+ n: nil,
+ s: &specs.Spec{},
+ expected: &NetworkSettings{
+ Ports: &nat.PortMap{},
+ Networks: map[string]*NetworkEndpointSettings{},
+ },
+ },
+ // Given native.NetNS with single Interface with Port Annotations, Return populated NetworkSettings
+ // UseCase: Inspect a Running Container with published ports
+ {
+ name: "Given NetNS with single Interface with Port Annotation, Return populated NetworkSettings",
+ n: &native.NetNS{
+ Interfaces: []native.NetInterface{
+ {
+ Interface: net.Interface{
+ Index: 1,
+ MTU: 1500,
+ Name: "eth0.100",
+ Flags: net.FlagUp,
+ },
+ HardwareAddr: "xx:xx:xx:xx:xx:xx",
+ Flags: []string{},
+ Addrs: []string{"10.0.4.30/24"},
+ },
+ },
+ },
+ s: &specs.Spec{
+ Annotations: map[string]string{
+ "nerdctl/ports": "[{\"HostPort\":8075,\"ContainerPort\":77,\"Protocol\":\"tcp\",\"HostIP\":\"127.0.0.1\"}]",
+ },
+ },
+ expected: &NetworkSettings{
+ Ports: &nat.PortMap{
+ nat.Port("77/tcp"): []nat.PortBinding{
+ {
+ HostIP: "127.0.0.1",
+ HostPort: "8075",
+ },
+ },
+ },
+ Networks: map[string]*NetworkEndpointSettings{
+ "unknown-eth0.100": {
+ IPAddress: "10.0.4.30",
+ IPPrefixLen: 24,
+ MacAddress: "xx:xx:xx:xx:xx:xx",
+ },
+ },
+ },
+ },
+ // Given native.NetNS with single Interface without Port Annotations, Return valid NetworkSettings w/ empty Ports
+ // UseCase: Inspect a Running Container without published ports
+ {
+ name: "Given NetNS with single Interface without Port Annotations, Return valid NetworkSettings w/ empty Ports",
+ n: &native.NetNS{
+ Interfaces: []native.NetInterface{
+ {
+ Interface: net.Interface{
+ Index: 1,
+ MTU: 1500,
+ Name: "eth0.100",
+ Flags: net.FlagUp,
+ },
+ HardwareAddr: "xx:xx:xx:xx:xx:xx",
+ Flags: []string{},
+ Addrs: []string{"10.0.4.30/24"},
+ },
+ },
+ },
+ s: &specs.Spec{
+ Annotations: map[string]string{},
+ },
+ expected: &NetworkSettings{
+ Ports: &nat.PortMap{},
+ Networks: map[string]*NetworkEndpointSettings{
+ "unknown-eth0.100": {
+ IPAddress: "10.0.4.30",
+ IPPrefixLen: 24,
+ MacAddress: "xx:xx:xx:xx:xx:xx",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tc := range testcase {
+ t.Run(tc.name, func(tt *testing.T) {
+ d, _ := networkSettingsFromNative(tc.n, tc.s)
+ assert.DeepEqual(tt, d, tc.expected)
+ })
+ }
+}
diff --git a/pkg/inspecttypes/native/container.go b/pkg/inspecttypes/native/container.go
index a47b7de174d..de015dd5f94 100644
--- a/pkg/inspecttypes/native/container.go
+++ b/pkg/inspecttypes/native/container.go
@@ -19,8 +19,8 @@ package native
import (
"net"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/containers"
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
)
// Container corresponds to a containerd-native container object.
diff --git a/pkg/inspecttypes/native/image.go b/pkg/inspecttypes/native/image.go
index 5eb5499b60d..d7e1ac388d9 100644
--- a/pkg/inspecttypes/native/image.go
+++ b/pkg/inspecttypes/native/image.go
@@ -17,8 +17,9 @@
package native
import (
- "github.com/containerd/containerd/images"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ "github.com/containerd/containerd/v2/core/images"
)
// Image corresponds to a containerd-native image object.
diff --git a/pkg/ipcutil/ipcutil.go b/pkg/ipcutil/ipcutil.go
new file mode 100644
index 00000000000..7fd3240f15c
--- /dev/null
+++ b/pkg/ipcutil/ipcutil.go
@@ -0,0 +1,252 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipcutil
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/docker/go-units"
+ "github.com/opencontainers/runtime-spec/specs-go"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/pkg/oci"
+
+ "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
+ "github.com/containerd/nerdctl/v2/pkg/labels"
+)
+
+type IPCMode string
+
+type IPC struct {
+ Mode IPCMode `json:"mode,omitempty"`
+ // VictimContainer is only used when mode is container
+ VictimContainerID *string `json:"victimContainerId,omitempty"`
+
+ // HostShmPath is only used when mode is shareable
+ HostShmPath *string `json:"hostShmPath,omitempty"`
+
+ // ShmSize is only used when mode is private or shareable
+ // Devshm size in bytes
+ ShmSize string `json:"shmSize,omitempty"`
+}
+
+const (
+ Private IPCMode = "private"
+ Host IPCMode = "host"
+ Shareable IPCMode = "shareable"
+ Container IPCMode = "container"
+)
+
+// DetectFlags detects IPC mode from the given ipc string and shmSize string.
+// If ipc is empty, it returns IPC{Mode: Private}.
+func DetectFlags(ctx context.Context, client *containerd.Client, stateDir string, ipc string, shmSize string) (IPC, error) {
+ var res IPC
+ res.ShmSize = shmSize
+ switch ipc {
+ case "", "private":
+ res.Mode = Private
+ case "host":
+ res.Mode = Host
+ case "shareable":
+ res.Mode = Shareable
+ shmPath := filepath.Join(stateDir, "shm")
+ res.HostShmPath = &shmPath
+ default: // container:
+ res.Mode = Container
+ parsed := strings.Split(ipc, ":")
+ if len(parsed) < 2 || parsed[0] != "container" {
+ return res, fmt.Errorf("invalid ipc namespace. Set --ipc=[host|container:")
+ }
+
+ containerName := parsed[1]
+ walker := &containerwalker.ContainerWalker{
+ Client: client,
+ OnFound: func(ctx context.Context, found containerwalker.Found) error {
+ if found.MatchCount > 1 {
+ return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
+ }
+ victimContainerID := found.Container.ID()
+ res.VictimContainerID = &victimContainerID
+
+ return nil
+ },
+ }
+ matchedCount, err := walker.Walk(ctx, containerName)
+ if err != nil {
+ return res, err
+ }
+ if matchedCount < 1 {
+ return res, fmt.Errorf("no such container: %s", containerName)
+ }
+ }
+
+ return res, nil
+}
+
+// EncodeIPCLabel encodes IPC spec into a label.
+func EncodeIPCLabel(ipc IPC) (string, error) {
+ if ipc.Mode == "" {
+ return "", nil
+ }
+ b, err := json.Marshal(ipc)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+// DecodeIPCLabel decodes IPC spec from a label.
+// For backward compatibility, if ipcLabel is empty, it returns IPC{Mode: Private}.
+func DecodeIPCLabel(ipcLabel string) (IPC, error) {
+ if ipcLabel == "" {
+ return IPC{
+ Mode: Private,
+ }, nil
+ }
+
+ var ipc IPC
+ if err := json.Unmarshal([]byte(ipcLabel), &ipc); err != nil {
+ return IPC{}, err
+ }
+ return ipc, nil
+}
+
+// GenerateIPCOpts generates IPC spec opts from the given IPC.
+func GenerateIPCOpts(ctx context.Context, ipc IPC, client *containerd.Client) ([]oci.SpecOpts, error) {
+ opts := make([]oci.SpecOpts, 0)
+
+ switch ipc.Mode {
+ case Private:
+ // If nothing is specified, or if private, default to normal behavior
+ if len(ipc.ShmSize) > 0 {
+ shmBytes, err := units.RAMInBytes(ipc.ShmSize)
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, oci.WithDevShmSize(shmBytes/1024))
+ }
+ case Host:
+ opts = append(opts, withBindMountHostIPC)
+ if runtime.GOOS != "windows" {
+ opts = append(opts, oci.WithHostNamespace(specs.IPCNamespace))
+ }
+ case Shareable:
+ if ipc.HostShmPath == nil {
+ return nil, errors.New("ipc mode is shareable, but host shm path is nil")
+ }
+ err := makeShareableDevshm(*ipc.HostShmPath, ipc.ShmSize)
+ if err != nil {
+ return nil, err
+ }
+ opts = append(opts, withBindMountHostOtherSourceIPC(*ipc.HostShmPath))
+ case Container:
+ if ipc.VictimContainerID == nil {
+ return nil, errors.New("ipc mode is container, but victim container id is nil")
+ }
+ targetCon, err := client.LoadContainer(ctx, *ipc.VictimContainerID)
+ if err != nil {
+ return nil, err
+ }
+
+ task, err := targetCon.Task(ctx, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ status, err := task.Status(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ if status.Status != containerd.Running {
+ return nil, fmt.Errorf("shared container is not running")
+ }
+
+ targetConLabels, err := targetCon.Labels(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ targetConIPC, err := DecodeIPCLabel(targetConLabels[labels.IPC])
+ if err != nil {
+ return nil, err
+ }
+
+ if targetConIPC.Mode == Host {
+ opts = append(opts, oci.WithHostNamespace(specs.IPCNamespace))
+ opts = append(opts, withBindMountHostIPC)
+ return opts, nil
+ } else if targetConIPC.Mode != Shareable {
+ return nil, errors.New("victim container's ipc mode is not shareable")
+ }
+
+ if targetConIPC.HostShmPath == nil {
+ return nil, errors.New("victim container's host shm path is nil")
+ }
+
+ opts = append(opts, withBindMountHostOtherSourceIPC(*targetConIPC.HostShmPath))
+ }
+
+ return opts, nil
+}
+
+// WithBindMountHostOtherSourceIPC replaces /dev/shm mount with rbind by the given path on host
+func withBindMountHostOtherSourceIPC(source string) oci.SpecOpts {
+ return func(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
+ for i, m := range s.Mounts {
+ p := path.Clean(m.Destination)
+ if p == "/dev/shm" {
+ s.Mounts[i] = specs.Mount{
+ Type: "bind",
+ Destination: p,
+ Source: source,
+ Options: []string{"rbind", "nosuid", "noexec", "nodev"},
+ }
+ }
+ }
+ return nil
+ }
+}
+
+// WithBindMountHostIPC replaces /dev/shm and /dev/mqueue mount with rbind.
+// Required for --ipc=host on rootless.
+func withBindMountHostIPC(_ context.Context, _ oci.Client, _ *containers.Container, s *oci.Spec) error {
+ for i, m := range s.Mounts {
+ switch p := path.Clean(m.Destination); p {
+ case "/dev/shm", "/dev/mqueue":
+ s.Mounts[i] = specs.Mount{
+ Destination: p,
+ Type: "bind",
+ Source: p,
+ Options: []string{"rbind", "nosuid", "noexec", "nodev"},
+ }
+ }
+ }
+ return nil
+}
+
+func CleanUp(ipc IPC) error {
+ return cleanUpPlatformSpecificIPC(ipc)
+}
diff --git a/pkg/ipcutil/ipcutil_linux.go b/pkg/ipcutil/ipcutil_linux.go
new file mode 100644
index 00000000000..0d1b9f6cbc6
--- /dev/null
+++ b/pkg/ipcutil/ipcutil_linux.go
@@ -0,0 +1,62 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipcutil
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/docker/go-units"
+ "golang.org/x/sys/unix"
+)
+
+// makeShareableDevshm returns devshm directory path on host when there is no error.
+func makeShareableDevshm(shmPath, shmSize string) error {
+ shmproperty := "mode=1777"
+ if len(shmSize) > 0 {
+ shmBytes, err := units.RAMInBytes(shmSize)
+ if err != nil {
+ return err
+ }
+ shmproperty = fmt.Sprintf("%s,size=%d", shmproperty, shmBytes)
+ }
+ err := os.MkdirAll(shmPath, 0700)
+ if err != nil {
+ return err
+ }
+ err = unix.Mount("/dev/shm", shmPath, "tmpfs", uintptr(unix.MS_NOEXEC|unix.MS_NOSUID|unix.MS_NODEV), shmproperty)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// cleanUpPlatformSpecificIPC cleans up platform specific IPC.
+func cleanUpPlatformSpecificIPC(ipc IPC) error {
+ if ipc.Mode == Shareable && ipc.HostShmPath != nil {
+ err := unix.Unmount(*ipc.HostShmPath, 0)
+ if err != nil {
+ return err
+ }
+ err = os.RemoveAll(*ipc.HostShmPath)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/cmd/nerdctl/network_create_windows.go b/pkg/ipcutil/ipcutil_other.go
similarity index 55%
rename from cmd/nerdctl/network_create_windows.go
rename to pkg/ipcutil/ipcutil_other.go
index 2d6acf7e92b..a4c25963cc0 100644
--- a/cmd/nerdctl/network_create_windows.go
+++ b/pkg/ipcutil/ipcutil_other.go
@@ -1,3 +1,5 @@
+//go:build !(linux || windows)
+
/*
Copyright The containerd Authors.
@@ -14,19 +16,19 @@
limitations under the License.
*/
-package main
-
-import "github.com/spf13/cobra"
+package ipcutil
-const (
- DefaultNetworkDriver = "nat"
-)
+import "fmt"
-func shellCompleteNetworkDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- candidates := []string{"nat"}
- return candidates, cobra.ShellCompDirectiveNoFileComp
+// makeShareableDevshm returns devshm directory path on host when there is no error.
+func makeShareableDevshm(shmPath, shmSize string) error {
+ return fmt.Errorf("unix does not support shareable devshm")
}
-func shellCompleteIPAMDrivers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
- return []string{"default"}, cobra.ShellCompDirectiveNoFileComp
+// cleanUpPlatformSpecificIPC cleans up platform specific IPC.
+func cleanUpPlatformSpecificIPC(ipc IPC) error {
+ if ipc.Mode == Shareable {
+ return fmt.Errorf("unix does not support shareable devshm")
+ }
+ return nil
}
diff --git a/pkg/ipcutil/ipcutil_windows.go b/pkg/ipcutil/ipcutil_windows.go
new file mode 100644
index 00000000000..5e9d4aa391d
--- /dev/null
+++ b/pkg/ipcutil/ipcutil_windows.go
@@ -0,0 +1,32 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package ipcutil
+
+import "fmt"
+
+// makeShareableDevshm returns devshm directory path on host when there is no error.
+func makeShareableDevshm(shmPath, shmSize string) error {
+ return fmt.Errorf("windows does not support shareable devshm")
+}
+
+// cleanUpPlatformSpecificIPC cleans up platform specific IPC.
+func cleanUpPlatformSpecificIPC(ipc IPC) error {
+ if ipc.Mode == Shareable {
+ return fmt.Errorf("windows does not support shareable devshm")
+ }
+ return nil
+}
diff --git a/pkg/ipfs/image.go b/pkg/ipfs/image.go
index 931c673cac0..84d73bda32e 100644
--- a/pkg/ipfs/image.go
+++ b/pkg/ipfs/image.go
@@ -22,30 +22,32 @@ import (
"io"
"os"
- "github.com/containerd/containerd"
- "github.com/containerd/containerd/images"
- "github.com/containerd/containerd/images/converter"
- "github.com/containerd/containerd/remotes"
- "github.com/containerd/nerdctl/pkg/idutil/imagewalker"
- "github.com/containerd/nerdctl/pkg/imgutil"
- "github.com/containerd/nerdctl/pkg/platformutil"
- "github.com/containerd/nerdctl/pkg/referenceutil"
+ ocispec "github.com/opencontainers/image-spec/specs-go/v1"
+
+ containerd "github.com/containerd/containerd/v2/client"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/containerd/v2/core/images/converter"
+ "github.com/containerd/containerd/v2/core/remotes"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
"github.com/containerd/stargz-snapshotter/ipfs"
ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client"
- "github.com/docker/docker/errdefs"
- ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/nerdctl/v2/pkg/api/types"
+ "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker"
+ "github.com/containerd/nerdctl/v2/pkg/imgutil"
+ "github.com/containerd/nerdctl/v2/pkg/platformutil"
)
const ipfsPathEnv = "IPFS_PATH"
// EnsureImage pull the specified image from IPFS.
-func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr io.Writer, snapshotter string, scheme string, ref string, mode imgutil.PullMode, ocispecPlatforms []ocispec.Platform, unpack *bool, quiet bool, ipfsPath string) (*imgutil.EnsuredImage, error) {
- switch mode {
+func EnsureImage(ctx context.Context, client *containerd.Client, scheme, ref, ipfsPath string, options types.ImagePullOptions) (*imgutil.EnsuredImage, error) {
+ switch options.Mode {
case "always", "missing", "never":
// NOP
default:
- return nil, fmt.Errorf("unexpected pull mode: %q", mode)
+ return nil, fmt.Errorf("unexpected pull mode: %q", options.Mode)
}
switch scheme {
case "ipfs", "ipns":
@@ -55,15 +57,15 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr
}
// if not `always` pull and given one platform and image found locally, return existing image directly.
- if mode != "always" && len(ocispecPlatforms) == 1 {
- if res, err := imgutil.GetExistingImage(ctx, client, snapshotter, ref, ocispecPlatforms[0]); err == nil {
+ if options.Mode != "always" && len(options.OCISpecPlatform) == 1 {
+ if res, err := imgutil.GetExistingImage(ctx, client, options.GOptions.Snapshotter, ref, options.OCISpecPlatform[0]); err == nil {
return res, nil
} else if !errdefs.IsNotFound(err) {
return nil, err
}
}
- if mode == "never" {
+ if options.Mode == "never" {
return nil, fmt.Errorf("image %q is not available", ref)
}
r, err := ipfs.NewResolver(ipfs.ResolverOptions{
@@ -73,7 +75,7 @@ func EnsureImage(ctx context.Context, client *containerd.Client, stdout, stderr
if err != nil {
return nil, err
}
- return imgutil.PullImage(ctx, client, stdout, stderr, snapshotter, r, ref, ocispecPlatforms, unpack, quiet)
+ return imgutil.PullImage(ctx, client, r, ref, options)
}
// Push pushes the specified image to IPFS.
@@ -85,16 +87,12 @@ func Push(ctx context.Context, client *containerd.Client, rawRef string, layerCo
ipath := lookupIPFSPath(ipfsPath)
if ensureImage {
// Ensure image contents are fully downloaded
- logrus.Infof("ensuring image contents")
+ log.G(ctx).Infof("ensuring image contents")
if err := ensureContentsOfIPFSImage(ctx, client, rawRef, allPlatforms, platform, ipath); err != nil {
- logrus.WithError(err).Warnf("failed to ensure the existence of image %q", rawRef)
+ log.G(ctx).WithError(err).Warnf("failed to ensure the existence of image %q", rawRef)
}
}
- ref, err := referenceutil.ParseAny(rawRef)
- if err != nil {
- return "", err
- }
- return ipfs.PushWithIPFSPath(ctx, client, ref.String(), layerConvert, platMC, &ipath)
+ return ipfs.PushWithIPFSPath(ctx, client, rawRef, layerConvert, platMC, &ipath)
}
// ensureContentsOfIPFSImage ensures that the entire contents of an existing IPFS image are fully downloaded to containerd.
diff --git a/pkg/ipfs/registry.go b/pkg/ipfs/registry.go
index 6dfc9687879..038b44f70ce 100644
--- a/pkg/ipfs/registry.go
+++ b/pkg/ipfs/registry.go
@@ -25,16 +25,17 @@ import (
"io"
"net/http"
"regexp"
+ "strconv"
"strings"
"time"
- "github.com/containerd/containerd/content"
- "github.com/containerd/containerd/images"
- ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client"
- "github.com/hashicorp/go-multierror"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/containerd/v2/core/content"
+ "github.com/containerd/containerd/v2/core/images"
+ "github.com/containerd/log"
+ ipfsclient "github.com/containerd/stargz-snapshotter/ipfs/client"
)
// RegistryOptions represents options to configure the registry.
@@ -72,22 +73,22 @@ var blobsRegexp = regexp.MustCompile(`/v2/ipfs/([a-z0-9]+)/blobs/(.*)`)
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cid, content, mediaType, size, err := s.serve(r)
if err != nil {
- logrus.WithError(err).Warnf("failed to serve %q %q", r.Method, r.URL.Path)
+ log.L.WithError(err).Warnf("failed to serve %q %q", r.Method, r.URL.Path)
// TODO: support response body following OCI Distribution Spec's error response format spec:
// https://github.com/opencontainers/distribution-spec/blob/v1.0/spec.md#error-codes
http.Error(w, "", http.StatusNotFound)
return
}
if content == nil {
- logrus.Debugf("returning without contents")
+ log.L.Debugf("returning without contents")
w.WriteHeader(200)
return
}
w.Header().Set("Content-Type", mediaType)
- w.Header().Set("Content-Length", fmt.Sprintf("%d", size))
+ w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
if r.Method == "GET" {
http.ServeContent(w, r, "", time.Now(), content)
- logrus.WithField("CID", cid).Debugf("served file")
+ log.L.WithField("CID", cid).Debugf("served file")
}
}
@@ -97,7 +98,7 @@ func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, e
}
if r.URL.Path == "/v2/" {
- logrus.Debugf("requested /v2/")
+ log.L.Debugf("requested /v2/")
return "", nil, "", 0, nil
}
@@ -108,7 +109,7 @@ func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, e
if !images.IsManifestType(mediaType) && !images.IsIndexType(mediaType) {
return "", nil, "", 0, fmt.Errorf("cannot serve non-manifest from manifest API: %q", mediaType)
}
- logrus.WithField("root CID", cidStr).WithField("digest", ref).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by digest")
+ log.L.WithField("root CID", cidStr).WithField("digest", ref).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by digest")
return resolvedCID, content, mediaType, size, err
}
if ref != "latest" {
@@ -118,7 +119,7 @@ func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, e
if err != nil {
return "", nil, "", 0, err
}
- logrus.WithField("root CID", cidStr).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by cid")
+ log.L.WithField("root CID", cidStr).WithField("resolved CID", resolvedCID).Debugf("resolved manifest by cid")
return resolvedCID, content, mediaType, size, nil
}
@@ -128,7 +129,7 @@ func (s *server) serve(r *http.Request) (string, io.ReadSeeker, string, int64, e
if err != nil {
return "", nil, "", 0, err
}
- logrus.WithField("root CID", rootCIDStr).WithField("digest", dgstStr).WithField("resolved CID", resolvedCID).Debugf("resolved blob by digest")
+ log.L.WithField("root CID", rootCIDStr).WithField("digest", dgstStr).WithField("resolved CID", resolvedCID).Debugf("resolved blob by digest")
return resolvedCID, content, mediaType, size, nil
}
@@ -233,15 +234,16 @@ func (s *server) resolveCIDOfDigest(ctx context.Context, dgst digest.Digest, des
if err != nil {
return "", ocispec.Descriptor{}, err
}
- var allErr error
+ var errs []error
for _, desc := range descs {
gotCID, gotDesc, err := s.resolveCIDOfDigest(ctx, dgst, desc)
if err != nil {
- allErr = multierror.Append(allErr, err)
+ errs = append(errs, err)
continue
}
return gotCID, gotDesc, nil
}
+ allErr := errors.Join(errs...)
if allErr == nil {
return "", ocispec.Descriptor{}, fmt.Errorf("not found")
}
diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go
index 58c747a559e..e2dd6ede16d 100644
--- a/pkg/labels/labels.go
+++ b/pkg/labels/labels.go
@@ -15,7 +15,7 @@
*/
// Package labels defines labels that are set to containerd containers as labels.
-// The labels are also passed to OCI containers as annotations.
+// The labels defined in this package are also passed to OCI containers as annotations.
package labels
const (
@@ -54,12 +54,15 @@ const (
// Currently, the length of the slice must be 1.
Networks = Prefix + "networks"
- // Ports is a JSON-marshalled string of []gocni.PortMapping .
+ // Ports is a JSON-marshalled string of []cni.PortMapping .
Ports = Prefix + "ports"
// IPAddress is the static IP address of the container assigned by the user
IPAddress = Prefix + "ip"
+ // IP6Address is the static IP6 address of the container assigned by the user
+ IP6Address = Prefix + "ip6"
+
// LogURI is the log URI
LogURI = Prefix + "log-uri"
@@ -76,19 +79,18 @@ const (
// Mounts is the mount points for the container.
Mounts = Prefix + "mounts"
- // Bypass4netns is the flag for acceleration with bypass4netns
- // Boolean value which can be parsed with strconv.ParseBool() is required.
- // (like "nerdctl/bypass4netns=true" or "nerdctl/bypass4netns=false")
- Bypass4netns = Prefix + "bypass4netns"
-
// StopTimeout is seconds to wait for stop a container.
- StopTimout = Prefix + "stop-timeout"
+ StopTimeout = Prefix + "stop-timeout"
MACAddress = Prefix + "mac-address"
// PIDContainer is the `nerdctl run --pid` for restarting
PIDContainer = Prefix + "pid-container"
+ // IPC is the `nerectl run --ipc` for restrating
+ // IPC indicates ipc victim container.
+ IPC = Prefix + "ipc"
+
// Error encapsulates a container human-readable string
// that describes container error.
Error = Prefix + "error"
@@ -98,10 +100,7 @@ const (
// Boolean value which can be parsed with strconv.ParseBool() is required.
// (like "nerdctl/default-network=true" or "nerdctl/default-network=false")
NerdctlDefaultNetwork = Prefix + "default-network"
-)
-var ShellCompletions = []string{
- Bypass4netns + "=true",
- Bypass4netns + "=false",
- // Other labels should not be set via CLI
-}
+ // ContainerAutoRemove is to check whether the --rm option is specified.
+ ContainerAutoRemove = Prefix + "auto-remove"
+)
diff --git a/pkg/lockutil/lockutil_unix.go b/pkg/lockutil/lockutil_unix.go
index 4f8a2a06a8e..de99ca9a4e1 100644
--- a/pkg/lockutil/lockutil_unix.go
+++ b/pkg/lockutil/lockutil_unix.go
@@ -1,4 +1,4 @@
-//go:build freebsd || linux
+//go:build unix
/*
Copyright The containerd Authors.
@@ -22,8 +22,9 @@ import (
"fmt"
"os"
- "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
+
+ "github.com/containerd/log"
)
func WithDirLock(dir string, fn func() error) error {
@@ -32,18 +33,18 @@ func WithDirLock(dir string, fn func() error) error {
return err
}
defer dirFile.Close()
- if err := Flock(dirFile, unix.LOCK_EX); err != nil {
+ if err := flock(dirFile, unix.LOCK_EX); err != nil {
return fmt.Errorf("failed to lock %q: %w", dir, err)
}
defer func() {
- if err := Flock(dirFile, unix.LOCK_UN); err != nil {
- logrus.WithError(err).Errorf("failed to unlock %q", dir)
+ if err := flock(dirFile, unix.LOCK_UN); err != nil {
+ log.L.WithError(err).Errorf("failed to unlock %q", dir)
}
}()
return fn()
}
-func Flock(f *os.File, flags int) error {
+func flock(f *os.File, flags int) error {
fd := int(f.Fd())
for {
err := unix.Flock(fd, flags)
@@ -52,3 +53,24 @@ func Flock(f *os.File, flags int) error {
}
}
}
+
+func Lock(dir string) (*os.File, error) {
+ dirFile, err := os.Open(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = flock(dirFile, unix.LOCK_EX); err != nil {
+ return nil, err
+ }
+
+ return dirFile, nil
+}
+
+func Unlock(locked *os.File) error {
+ defer func() {
+ _ = locked.Close()
+ }()
+
+ return flock(locked, unix.LOCK_UN)
+}
diff --git a/pkg/lockutil/lockutil_windows.go b/pkg/lockutil/lockutil_windows.go
index bcfd9776556..205efde83f5 100644
--- a/pkg/lockutil/lockutil_windows.go
+++ b/pkg/lockutil/lockutil_windows.go
@@ -20,8 +20,9 @@ import (
"fmt"
"os"
- "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
+
+ "github.com/containerd/log"
)
func WithDirLock(dir string, fn func() error) error {
@@ -31,15 +32,34 @@ func WithDirLock(dir string, fn func() error) error {
}
defer dirFile.Close()
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
- // 1 lock immediately
- if err = windows.LockFileEx(windows.Handle(dirFile.Fd()), 1, 0, 1, 0, &windows.Overlapped{}); err != nil {
+ if err = windows.LockFileEx(windows.Handle(dirFile.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil {
return fmt.Errorf("failed to lock %q: %w", dir, err)
}
defer func() {
- if err := windows.UnlockFileEx(windows.Handle(dirFile.Fd()), 0, 1, 0, &windows.Overlapped{}); err != nil {
- logrus.WithError(err).Errorf("failed to unlock %q", dir)
+ if err := windows.UnlockFileEx(windows.Handle(dirFile.Fd()), 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil {
+ log.L.WithError(err).Errorf("failed to unlock %q", dir)
}
}()
return fn()
}
+
+func Lock(dir string) (*os.File, error) {
+ dirFile, err := os.OpenFile(dir+".lock", os.O_CREATE, 0644)
+ if err != nil {
+ return nil, err
+ }
+ // see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
+ if err = windows.LockFileEx(windows.Handle(dirFile.Fd()), windows.LOCKFILE_EXCLUSIVE_LOCK, 0, ^uint32(0), ^uint32(0), new(windows.Overlapped)); err != nil {
+ return nil, fmt.Errorf("failed to lock %q: %w", dir, err)
+ }
+ return dirFile, nil
+}
+
+func Unlock(locked *os.File) error {
+ defer func() {
+ _ = locked.Close()
+ }()
+
+ return windows.UnlockFileEx(windows.Handle(locked.Fd()), 0, ^uint32(0), ^uint32(0), new(windows.Overlapped))
+}
diff --git a/pkg/logging/cri_logger.go b/pkg/logging/cri_logger.go
index c47606b2aa0..cf582d3c887 100644
--- a/pkg/logging/cri_logger.go
+++ b/pkg/logging/cri_logger.go
@@ -25,6 +25,7 @@ package logging
import (
"bufio"
"bytes"
+ "context"
"errors"
"fmt"
"io"
@@ -33,9 +34,11 @@ import (
"path/filepath"
"time"
- "github.com/containerd/containerd/log"
- "github.com/containerd/nerdctl/pkg/logging/tail"
- "github.com/sirupsen/logrus"
+ "github.com/fsnotify/fsnotify"
+
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/logging/tail"
)
// LogStreamType is the type of the stream in CRI container log.
@@ -46,6 +49,9 @@ const (
Stdout LogStreamType = "stdout"
// Stderr is the stream type for stderr.
Stderr LogStreamType = "stderr"
+
+ // logForceCheckPeriod is the period to check for a new read
+ logForceCheckPeriod = 1 * time.Second
)
// LogTag is the tag of a log line in CRI container log.
@@ -90,7 +96,9 @@ func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan o
if err != nil {
return fmt.Errorf("failed to open log file %q: %v", logPath, err)
}
- defer f.Close()
+ defer func() {
+ f.Close()
+ }()
// Search start point based on tail line.
start, err := tail.FindTailLineStartIndex(f, opts.Tail)
@@ -102,6 +110,8 @@ func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan o
return fmt.Errorf("failed to seek in log file %q: %v", logPath, err)
}
+ var watcher *fsnotify.Watcher
+
limitedMode := (opts.Tail > 0) && (!opts.Follow)
limitedNum := opts.Tail
// Start parsing the logs.
@@ -111,14 +121,17 @@ func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan o
isNewLine := true
writer := newLogWriter(stdout, stderr, opts)
msg := &logMessage{}
+ baseName := filepath.Base(logPath)
+ dir := filepath.Dir(logPath)
+
for {
select {
case <-stopChannel:
- logrus.Debugf("received stop signal while reading cri logfile, returning")
+ log.L.Debugf("received stop signal while reading cri logfile, returning")
return nil
default:
if stop || (limitedMode && limitedNum == 0) {
- logrus.Debugf("finished parsing log file, path: %s", logPath)
+ log.L.Debugf("finished parsing log file, path: %s", logPath)
return nil
}
l, err := r.ReadBytes(eol[0])
@@ -127,13 +140,48 @@ func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan o
return fmt.Errorf("failed to read log file %q: %v", logPath, err)
}
if opts.Follow {
-
// Reset seek so that if this is an incomplete line,
// it will be read again.
if _, err := f.Seek(-int64(len(l)), io.SeekCurrent); err != nil {
return fmt.Errorf("failed to reset seek in log file %q: %v", logPath, err)
}
+ if watcher == nil {
+ // Initialize the watcher if it has not been initialized yet.
+ if watcher, err = NewLogFileWatcher(dir); err != nil {
+ return err
+ }
+ defer watcher.Close()
+ // If we just created the watcher, try again to read as we might have missed
+ // the event.
+ continue
+ }
+
+ var recreated bool
+ // Wait until the next log change.
+ recreated, err = startTail(context.Background(), baseName, watcher)
+ if err != nil {
+ return err
+ }
+ if recreated {
+ newF, err := openFileShareDelete(logPath)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ //If the user application outputs logs too quickly,
+ //There is a slight possibility that nerdctl has just rotated the log file,
+ //try opening it once more.
+ time.Sleep(10 * time.Millisecond)
+ }
+ newF, err = openFileShareDelete(logPath)
+ if err != nil {
+ return fmt.Errorf("failed to open cri logfile %q: %w", logPath, err)
+ }
+ }
+ f.Close()
+ f = newF
+ r = bufio.NewReader(f)
+ }
+
// If the container exited consume data until the next EOF
continue
}
@@ -142,22 +190,22 @@ func ReadLogs(opts *LogViewOptions, stdout, stderr io.Writer, stopChannel chan o
if len(l) == 0 {
continue
}
- logrus.Debugf("incomplete line in log file, path: %s, line: %s", logPath, l)
+ log.L.Debugf("incomplete line in log file, path: %s, line: %s", logPath, l)
}
// Parse the log line.
msg.reset()
if err := ParseCRILog(l, msg); err != nil {
- logrus.WithError(err).Errorf("failed when parsing line in log file, path: %s, line: %s", logPath, l)
+ log.L.WithError(err).Errorf("failed when parsing line in log file, path: %s, line: %s", logPath, l)
continue
}
// Write the log line into the stream.
if err := writer.write(msg, isNewLine); err != nil {
if err == errMaximumWrite {
- logrus.Debugf("finished parsing log file, hit bytes limit path: %s", logPath)
+ log.L.Debugf("finished parsing log file, hit bytes limit path: %s", logPath)
return nil
}
- logrus.WithError(err).Errorf("failed when writing line to log file, path: %s, line: %s", logPath, l)
+ log.L.WithError(err).Errorf("failed when writing line to log file, path: %s, line: %s", logPath, l)
return err
}
if limitedMode {
diff --git a/pkg/logging/cri_logger_test.go b/pkg/logging/cri_logger_test.go
index fff79506ae6..6d45e4999bc 100644
--- a/pkg/logging/cri_logger_test.go
+++ b/pkg/logging/cri_logger_test.go
@@ -28,7 +28,9 @@ import (
"fmt"
"io"
"os"
+ "path/filepath"
"reflect"
+ "runtime"
"testing"
"time"
)
@@ -89,7 +91,7 @@ func TestReadLogs(t *testing.T) {
err = ReadLogs(&tc.logViewOptions, stdoutBuf, stderrBuf, stopChan)
if err != nil {
- t.Fatalf(err.Error())
+ t.Fatal(err.Error())
}
if stderrBuf.Len() > 0 {
t.Fatalf("Stderr: %v", stderrBuf.String())
@@ -157,9 +159,8 @@ func TestParseLog(t *testing.T) {
if err != nil {
if test.err {
continue
- } else {
- t.Errorf("ParseCRILog err %s ", err.Error())
}
+ t.Errorf("ParseCRILog err %s ", err.Error())
}
if !reflect.DeepEqual(test.msg, logmsg) {
@@ -231,3 +232,88 @@ func TestReadLogsLimitsWithTimestamps(t *testing.T) {
t.Errorf("should have two lines, lineCount= %d", lineCount)
}
}
+
+func TestReadRotatedLog(t *testing.T) {
+ tmpDir := t.TempDir()
+ if runtime.GOOS == "windows" {
+ t.Skip("windows implementation does not seem to work right now and should be fixed: https://github.com/containerd/nerdctl/issues/3554")
+ }
+ file, err := os.CreateTemp(tmpDir, "logfile")
+ if err != nil {
+ t.Errorf("unable to create temp file, error: %s", err.Error())
+ }
+ stdoutBuf := &bytes.Buffer{}
+ stderrBuf := &bytes.Buffer{}
+ containerStoped := make(chan os.Signal)
+ // Start to follow the container's log.
+ fileName := file.Name()
+ go func() {
+ lvOpts := &LogViewOptions{
+ Follow: true,
+ LogPath: fileName,
+ }
+ _ = ReadLogs(lvOpts, stdoutBuf, stderrBuf, containerStoped)
+ }()
+
+ // log in stdout
+ expectedStdout := "line0line2line4line6line8"
+ // log in stderr
+ expectedStderr := "line1line3line5line7line9"
+
+ dir := filepath.Dir(file.Name())
+ baseName := filepath.Base(file.Name())
+
+ // Write 10 lines to log file.
+ // Let ReadLogs start.
+ time.Sleep(50 * time.Millisecond)
+
+ for line := 0; line < 10; line++ {
+ // Write the first three lines to log file
+ now := time.Now().Format(time.RFC3339Nano)
+ if line%2 == 0 {
+ file.WriteString(fmt.Sprintf(
+ "%s stdout P line%d\n", now, line))
+ } else {
+ file.WriteString(fmt.Sprintf(
+ "%s stderr P line%d\n", now, line))
+ }
+
+ time.Sleep(1 * time.Millisecond)
+
+ if line == 5 {
+ file.Close()
+ // Pretend to rotate the log.
+ rotatedName := fmt.Sprintf("%s.%s", baseName, time.Now().Format("220060102-150405"))
+ rotatedName = filepath.Join(dir, rotatedName)
+ if err := os.Rename(filepath.Join(dir, baseName), rotatedName); err != nil {
+ t.Errorf("failed to rotate log %q to %q, error: %s", file.Name(), rotatedName, err.Error())
+ return
+ }
+
+ time.Sleep(20 * time.Millisecond)
+ newF := filepath.Join(dir, baseName)
+ if file, err = os.Create(newF); err != nil {
+ t.Errorf("unable to create new log file, error: %s", err.Error())
+ return
+ }
+ }
+ }
+
+ // Finished writing into the file, close it, so we can delete it later.
+ err = file.Close()
+ if err != nil {
+ t.Errorf("could not close file, error: %s", err.Error())
+ }
+
+ time.Sleep(2 * time.Second)
+ // Make the function ReadLogs end.
+ close(containerStoped)
+
+ if expectedStdout != stdoutBuf.String() {
+ t.Errorf("expected: %s, acoutal: %s", expectedStdout, stdoutBuf.String())
+ }
+
+ if expectedStderr != stderrBuf.String() {
+ t.Errorf("expected: %s, acoutal: %s", expectedStderr, stderrBuf.String())
+ }
+}
diff --git a/pkg/logging/fluentd_logger.go b/pkg/logging/fluentd_logger.go
index 402a26292b8..c0a7d579fdb 100644
--- a/pkg/logging/fluentd_logger.go
+++ b/pkg/logging/fluentd_logger.go
@@ -17,6 +17,7 @@
package logging
import (
+ "context"
"fmt"
"math"
"net/url"
@@ -26,10 +27,12 @@ import (
"sync"
"time"
- "github.com/containerd/containerd/runtime/v2/logging"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/fluent/fluent-logger-golang/fluent"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
type FluentdLogger struct {
@@ -77,11 +80,11 @@ const (
func FluentdLogOptsValidate(logOptMap map[string]string) error {
for key := range logOptMap {
if !strutil.InStringSlice(FluentdLogOpts, key) {
- logrus.Warnf("log-opt %s is ignored for fluentd log driver", key)
+ log.L.Warnf("log-opt %s is ignored for fluentd log driver", key)
}
}
if _, ok := logOptMap[fluentAddress]; !ok {
- logrus.Warnf("%s is missing for fluentd log driver, the default value %s:%d will be used", fluentAddress, defaultHost, defaultPort)
+ log.L.Warnf("%s is missing for fluentd log driver, the default value %s:%d will be used", fluentAddress, defaultHost, defaultPort)
}
return nil
}
@@ -97,7 +100,7 @@ func (f *FluentdLogger) Init(dataStore, ns, id string) error {
return nil
}
-func (f *FluentdLogger) PreProcess(_ string, config *logging.Config) error {
+func (f *FluentdLogger) PreProcess(_ context.Context, _ string, config *logging.Config) error {
if runtime.GOOS == "windows" {
// TODO: support fluentd on windows
return fmt.Errorf("logging to fluentd is not supported on windows")
diff --git a/pkg/logging/journald_logger.go b/pkg/logging/journald_logger.go
index f14aa651b70..404d4abd02f 100644
--- a/pkg/logging/journald_logger.go
+++ b/pkg/logging/journald_logger.go
@@ -18,6 +18,7 @@ package logging
import (
"bytes"
+ "context"
"errors"
"fmt"
"io"
@@ -28,12 +29,16 @@ import (
"text/template"
"time"
- "github.com/containerd/containerd/runtime/v2/logging"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/coreos/go-systemd/v22/journal"
"github.com/docker/cli/templates"
timetypes "github.com/docker/docker/api/types/time"
- "github.com/sirupsen/logrus"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/clientutil"
+ "github.com/containerd/nerdctl/v2/pkg/containerutil"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
var JournalDriverLogOpts = []string{
@@ -43,15 +48,16 @@ var JournalDriverLogOpts = []string{
func JournalLogOptsValidate(logOptMap map[string]string) error {
for key := range logOptMap {
if !strutil.InStringSlice(JournalDriverLogOpts, key) {
- logrus.Warnf("log-opt %s is ignored for journald log driver", key)
+ log.L.Warnf("log-opt %s is ignored for journald log driver", key)
}
}
return nil
}
type JournaldLogger struct {
- Opts map[string]string
- vars map[string]string
+ Opts map[string]string
+ vars map[string]string
+ Address string
}
type identifier struct {
@@ -64,7 +70,7 @@ func (journaldLogger *JournaldLogger) Init(dataStore, ns, id string) error {
return nil
}
-func (journaldLogger *JournaldLogger) PreProcess(dataStore string, config *logging.Config) error {
+func (journaldLogger *JournaldLogger) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error {
if !journal.Enabled() {
return errors.New("the local systemd journal is not available for logging")
}
@@ -93,9 +99,37 @@ func (journaldLogger *JournaldLogger) PreProcess(dataStore string, config *loggi
syslogIdentifier = b.String()
}
}
+
+ client, ctx, cancel, err := clientutil.NewClient(ctx, config.Namespace, journaldLogger.Address)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ cancel()
+ client.Close()
+ }()
+ containerID := config.ID
+ container, err := client.LoadContainer(ctx, containerID)
+ if err != nil {
+ return err
+ }
+ containerLabels, err := container.Labels(ctx)
+ if err != nil {
+ return err
+ }
+ containerInfo, err := container.Info(ctx)
+ if err != nil {
+ return err
+ }
+
// construct log metadata for the container
vars := map[string]string{
"SYSLOG_IDENTIFIER": syslogIdentifier,
+ "CONTAINER_TAG": syslogIdentifier,
+ "CONTAINER_ID": shortID,
+ "CONTAINER_ID_FULL": containerID,
+ "CONTAINER_NAME": containerutil.GetContainerName(containerLabels),
+ "IMAGE_NAME": containerInfo.Image,
}
journaldLogger.vars = vars
return nil
@@ -141,7 +175,7 @@ func FetchLogs(stdout, stderr io.Writer, journalctlArgs []string, stopChannel ch
// Setup killing goroutine:
go func() {
<-stopChannel
- logrus.Debugf("killing journalctl logs process with PID: %#v", cmd.Process.Pid)
+ log.L.Debugf("killing journalctl logs process with PID: %#v", cmd.Process.Pid)
cmd.Process.Kill()
}()
@@ -172,7 +206,7 @@ func viewLogsJournald(lvopts LogViewOptions, stdout, stderr io.Writer, stopChann
journalctlArgs = append(journalctlArgs, "--since", date)
}
if lvopts.Timestamps {
- logrus.Warnf("unsupported Timestamps option for journald driver")
+ log.L.Warnf("unsupported Timestamps option for journald driver")
}
if lvopts.Until != "" {
// using GetTimestamp from moby to keep time format consistency
diff --git a/pkg/logging/json_logger.go b/pkg/logging/json_logger.go
index 61f84e2db34..988e847d5e9 100644
--- a/pkg/logging/json_logger.go
+++ b/pkg/logging/json_logger.go
@@ -17,21 +17,26 @@
package logging
import (
+ "context"
"errors"
"fmt"
"io"
"os"
- "os/exec"
"path/filepath"
"strconv"
"time"
- "github.com/containerd/containerd/runtime/v2/logging"
- "github.com/containerd/nerdctl/pkg/logging/jsonfile"
- "github.com/containerd/nerdctl/pkg/strutil"
"github.com/docker/go-units"
"github.com/fahedouch/go-logrotate"
- "github.com/sirupsen/logrus"
+ "github.com/fsnotify/fsnotify"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/logging/jsonfile"
+ "github.com/containerd/nerdctl/v2/pkg/logging/tail"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
var JSONDriverLogOpts = []string{
@@ -48,7 +53,7 @@ type JSONLogger struct {
func JSONFileLogOptsValidate(logOptMap map[string]string) error {
for key := range logOptMap {
if !strutil.InStringSlice(JSONDriverLogOpts, key) {
- logrus.Warnf("log-opt %s is ignored for json-file log driver", key)
+ log.L.Warnf("log-opt %s is ignored for json-file log driver", key)
}
}
return nil
@@ -73,7 +78,7 @@ func (jsonLogger *JSONLogger) Init(dataStore, ns, id string) error {
return nil
}
-func (jsonLogger *JSONLogger) PreProcess(dataStore string, config *logging.Config) error {
+func (jsonLogger *JSONLogger) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error {
var jsonFilePath string
if logPath, ok := jsonLogger.Opts[LogPath]; ok {
jsonFilePath = logPath
@@ -83,10 +88,11 @@ func (jsonLogger *JSONLogger) PreProcess(dataStore string, config *logging.Confi
l := &logrotate.Logger{
Filename: jsonFilePath,
}
- //maxSize Defaults to unlimited.
- var capVal int64
- capVal = -1
+ // MaxBytes is the maximum size in bytes of the log file before it gets
+ // rotated. If not set, it defaults to 100 MiB.
+ // see: https://github.com/fahedouch/go-logrotate/blob/6a8beddaea39b2b9c77109d7fa2fe92053c063e5/logrotate.go#L500
if capacity, ok := jsonLogger.Opts[MaxSize]; ok {
+ var capVal int64
var err error
capVal, err = units.FromHumanSize(capacity)
if err != nil {
@@ -95,8 +101,8 @@ func (jsonLogger *JSONLogger) PreProcess(dataStore string, config *logging.Confi
if capVal <= 0 {
return fmt.Errorf("max-size must be a positive number")
}
+ l.MaxBytes = capVal
}
- l.MaxBytes = capVal
maxFile := 1
if maxFileString, ok := jsonLogger.Opts[MaxFile]; ok {
var err error
@@ -127,12 +133,18 @@ func (jsonLogger *JSONLogger) PostProcess() error {
func viewLogsJSONFile(lvopts LogViewOptions, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
logFilePath := jsonfile.Path(lvopts.DatastoreRootPath, lvopts.Namespace, lvopts.ContainerID)
if _, err := os.Stat(logFilePath); err != nil {
- return fmt.Errorf("failed to stat JSON log file ")
+ // FIXME: this is a workaround for the actual issue, not a real solution
+ // https://github.com/containerd/nerdctl/issues/3187
+ if errors.Is(err, errdefs.ErrNotFound) {
+ log.L.Warnf("Racing log file creation. Pausing briefly.")
+ time.Sleep(200 * time.Millisecond)
+ _, err = os.Stat(logFilePath)
+ }
+ if err != nil {
+ return fmt.Errorf("failed to stat JSON log file %w", err)
+ }
}
- if checkExecutableAvailableInPath("tail") {
- return viewLogsJSONFileThroughTailExec(lvopts, logFilePath, stdout, stderr, stopChannel)
- }
return viewLogsJSONFileDirect(lvopts, logFilePath, stdout, stderr, stopChannel)
}
@@ -144,89 +156,100 @@ func viewLogsJSONFileDirect(lvopts LogViewOptions, jsonLogFilePath string, stdou
if err != nil {
return err
}
- defer fin.Close()
- err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, lvopts.Tail)
- if err != nil {
- return fmt.Errorf("error occurred while doing initial read of JSON logfile %q: %s", jsonLogFilePath, err)
- }
+ defer func() { fin.Close() }()
- if lvopts.Follow {
- // Get the current file handler's seek.
- lastPos, err := fin.Seek(0, io.SeekCurrent)
- if err != nil {
- return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
- }
- fin.Close()
- for {
- select {
- case <-stopChannel:
- logrus.Debugf("received stop signal while re-reading JSON logfile, returning")
+ // Search start point based on tail line.
+ start, err := tail.FindTailLineStartIndex(fin, lvopts.Tail)
+ if err != nil {
+ return fmt.Errorf("failed to tail %d lines of JSON logfile %q: %w", lvopts.Tail, jsonLogFilePath, err)
+ }
+
+ if _, err := fin.Seek(start, io.SeekStart); err != nil {
+ return fmt.Errorf("failed to seek in log file %q from %d position: %w", jsonLogFilePath, start, err)
+ }
+
+ limitedMode := (lvopts.Tail > 0) && (!lvopts.Follow)
+ limitedNum := lvopts.Tail
+ var stop bool
+ var watcher *fsnotify.Watcher
+ baseName := filepath.Base(jsonLogFilePath)
+ dir := filepath.Dir(jsonLogFilePath)
+ retryTimes := 2
+ backBytes := 0
+
+ for {
+ select {
+ case <-stopChannel:
+ log.L.Debug("received stop signal while re-reading JSON logfile, returning")
+ return nil
+ default:
+ if stop || (limitedMode && limitedNum == 0) {
+ log.L.Debugf("finished parsing log JSON filefile, path: %s", jsonLogFilePath)
return nil
- default:
- // Re-open the file and seek to the last-consumed offset.
- fin, err = os.OpenFile(jsonLogFilePath, os.O_RDONLY, 0400)
- if err != nil {
- fin.Close()
- return fmt.Errorf("error occurred while trying to re-open JSON logfile %q: %s", jsonLogFilePath, err)
+ }
+
+ if line, err := jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until); err != nil {
+ if len(line) > 0 {
+ time.Sleep(5 * time.Millisecond)
+ if retryTimes == 0 {
+ log.L.Infof("finished parsing log JSON filefile, path: %s, line: %s", jsonLogFilePath, string(line))
+ return fmt.Errorf("error occurred while doing read of JSON logfile %q: %s, retryTimes: %d", jsonLogFilePath, err, retryTimes)
+ }
+ retryTimes--
+ backBytes = len(line)
+ } else {
+ return fmt.Errorf("error occurred while doing read of JSON logfile %q: %s", jsonLogFilePath, err)
}
- _, err = fin.Seek(lastPos, 0)
+ } else {
+ retryTimes = 2
+ backBytes = 0
+ }
+
+ if lvopts.Follow {
+ // Get the current file handler's seek.
+ lastPos, err := fin.Seek(int64(-backBytes), io.SeekCurrent)
if err != nil {
- fin.Close()
return fmt.Errorf("error occurred while trying to seek JSON logfile %q at position %d: %s", jsonLogFilePath, lastPos, err)
}
- err = jsonfile.Decode(stdout, stderr, fin, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
- if err != nil {
- fin.Close()
- return fmt.Errorf("error occurred while doing follow-up decoding of JSON logfile %q at starting position %d: %s", jsonLogFilePath, lastPos, err)
+ if watcher == nil {
+ // Initialize the watcher if it has not been initialized yet.
+ if watcher, err = NewLogFileWatcher(dir); err != nil {
+ return err
+ }
+ defer watcher.Close()
+ // If we just created the watcher, try again to read as we might have missed
+ // the event.
+ continue
}
- // Record current file seek position before looping again.
- lastPos, err = fin.Seek(0, io.SeekCurrent)
+ var recreated bool
+ // Wait until the next log change.
+ recreated, err = startTail(context.Background(), baseName, watcher)
if err != nil {
+ return err
+ }
+ if recreated {
+ newF, err := openFileShareDelete(jsonLogFilePath)
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ //If the user application outputs logs too quickly,
+ //There is a slight possibility that nerdctl has just rotated the log file,
+ //try opening it once more.
+ time.Sleep(10 * time.Millisecond)
+ }
+ newF, err = openFileShareDelete(jsonLogFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to open JSON logfile %q: %w", jsonLogFilePath, err)
+ }
+ }
fin.Close()
- return fmt.Errorf("error occurred while trying to seek JSON logfile %q at current position: %s", jsonLogFilePath, err)
+ fin = newF
}
- fin.Close()
+ continue
}
+ stop = true
// Give the OS a second to breathe before re-opening the file:
- time.Sleep(time.Second)
}
}
- return nil
-}
-
-// Loads logs through the `tail` executable.
-func viewLogsJSONFileThroughTailExec(lvopts LogViewOptions, jsonLogFilePath string, stdout, stderr io.Writer, stopChannel chan os.Signal) error {
- var args []string
-
- args = append(args, "-n")
- if lvopts.Tail == 0 {
- args = append(args, "+0")
- } else {
- args = append(args, fmt.Sprintf("%d", lvopts.Tail))
- }
-
- if lvopts.Follow {
- args = append(args, "-f")
- }
- args = append(args, jsonLogFilePath)
- cmd := exec.Command("tail", args...)
- cmd.Stderr = os.Stderr
- r, err := cmd.StdoutPipe()
- if err != nil {
- return err
- }
- if err := cmd.Start(); err != nil {
- return err
- }
-
- // Setup killing goroutine:
- go func() {
- <-stopChannel
- logrus.Debugf("killing tail logs process with PID: %d", cmd.Process.Pid)
- cmd.Process.Kill()
- }()
-
- return jsonfile.Decode(stdout, stderr, r, lvopts.Timestamps, lvopts.Since, lvopts.Until, 0)
}
diff --git a/pkg/logging/json_logger_test.go b/pkg/logging/json_logger_test.go
new file mode 100644
index 00000000000..7d0be36285d
--- /dev/null
+++ b/pkg/logging/json_logger_test.go
@@ -0,0 +1,177 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package logging
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+ "time"
+)
+
+func TestReadRotatedJSONLog(t *testing.T) {
+ tmpDir := t.TempDir()
+ if runtime.GOOS == "windows" {
+ t.Skip("windows implementation does not seem to work right now and should be fixed: https://github.com/containerd/nerdctl/issues/3554")
+ }
+ file, err := os.CreateTemp(tmpDir, "logfile")
+ if err != nil {
+ t.Errorf("unable to create temp file, error: %s", err.Error())
+ }
+ stdoutBuf := &bytes.Buffer{}
+ stderrBuf := &bytes.Buffer{}
+ containerStopped := make(chan os.Signal)
+ // Start to follow the container's log.
+ fileName := file.Name()
+ go func() {
+ lvOpts := LogViewOptions{
+ Follow: true,
+ LogPath: fileName,
+ }
+ viewLogsJSONFileDirect(lvOpts, file.Name(), stdoutBuf, stderrBuf, containerStopped)
+ }()
+
+ // log in stdout
+ expectedStdout := "line0\nline1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\n"
+ dir := filepath.Dir(file.Name())
+ baseName := filepath.Base(file.Name())
+
+ // Write 10 lines to log file.
+ // Let ReadLogs start.
+ time.Sleep(50 * time.Millisecond)
+
+ type logContent struct {
+ Log string `json:"log"`
+ Stream string `json:"stream"`
+ Time string `json:"time"`
+ }
+
+ for line := 0; line < 10; line++ {
+ // Write the first three lines to log file
+ log := logContent{}
+ log.Log = fmt.Sprintf("line%d\n", line)
+ log.Stream = "stdout"
+ log.Time = time.Now().Format(time.RFC3339Nano)
+ time.Sleep(1 * time.Millisecond)
+ logData, _ := json.Marshal(log)
+ file.Write(logData)
+
+ if line == 5 {
+ file.Close()
+ // Pretend to rotate the log.
+ rotatedName := fmt.Sprintf("%s.%s", baseName, time.Now().Format("20060102-150405"))
+ rotatedName = filepath.Join(dir, rotatedName)
+ if err := os.Rename(filepath.Join(dir, baseName), rotatedName); err != nil {
+ t.Errorf("failed to rotate log %q to %q, error: %s", file.Name(), rotatedName, err.Error())
+ return
+ }
+
+ time.Sleep(20 * time.Millisecond)
+ newF := filepath.Join(dir, baseName)
+ if file, err = os.Create(newF); err != nil {
+ t.Errorf("unable to create new log file, error: %s", err.Error())
+ return
+ }
+ }
+ }
+
+ // Finished writing into the file, close it, so we can delete it later.
+ err = file.Close()
+ if err != nil {
+ t.Errorf("could not close file, error: %s", err.Error())
+ }
+
+ time.Sleep(2 * time.Second)
+ // Make the function ReadLogs end.
+ close(containerStopped)
+
+ if expectedStdout != stdoutBuf.String() {
+ t.Errorf("expected: %s, acoutal: %s", expectedStdout, stdoutBuf.String())
+ }
+}
+
+func TestReadJSONLogs(t *testing.T) {
+ file, err := os.CreateTemp("", "TestFollowLogs")
+ if err != nil {
+ t.Fatalf("unable to create temp file")
+ }
+ defer os.Remove(file.Name())
+ file.WriteString(`{"log":"line1\n","stream":"stdout","time":"2024-07-12T03:09:24.916296732Z"}` + "\n")
+ file.WriteString(`{"log":"line2\n","stream":"stdout","time":"2024-07-12T03:09:24.916296732Z"}` + "\n")
+ file.WriteString(`{"log":"line3\n","stream":"stdout","time":"2024-07-12T03:09:24.916296732Z"}` + "\n")
+
+ stopChan := make(chan os.Signal)
+ testCases := []struct {
+ name string
+ logViewOptions LogViewOptions
+ expected string
+ }{
+ {
+ name: "default log options should output all lines",
+ logViewOptions: LogViewOptions{
+ LogPath: file.Name(),
+ Tail: 0,
+ },
+ expected: "line1\nline2\nline3\n",
+ },
+ {
+ name: "using Tail 2 should output last 2 lines",
+ logViewOptions: LogViewOptions{
+ LogPath: file.Name(),
+ Tail: 2,
+ },
+ expected: "line2\nline3\n",
+ },
+ {
+ name: "using Tail 4 should output all lines when the log has less than 4 lines",
+ logViewOptions: LogViewOptions{
+ LogPath: file.Name(),
+ Tail: 4,
+ },
+ expected: "line1\nline2\nline3\n",
+ },
+ {
+ name: "using Tail 0 should output all",
+ logViewOptions: LogViewOptions{
+ LogPath: file.Name(),
+ Tail: 0,
+ },
+ expected: "line1\nline2\nline3\n",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ stdoutBuf := bytes.NewBuffer(nil)
+ stderrBuf := bytes.NewBuffer(nil)
+ err = viewLogsJSONFileDirect(tc.logViewOptions, file.Name(), stdoutBuf, stderrBuf, stopChan)
+
+ if err != nil {
+ t.Fatal(err.Error())
+ }
+ if stderrBuf.Len() > 0 {
+ t.Fatalf("Stderr: %v", stderrBuf.String())
+ }
+ if actual := stdoutBuf.String(); tc.expected != actual {
+ t.Fatalf("Actual output does not match expected.\nActual: %v\nExpected: %v\n", actual, tc.expected)
+ }
+ })
+ }
+}
diff --git a/pkg/logging/jsonfile/jsonfile.go b/pkg/logging/jsonfile/jsonfile.go
index cf5400daca5..2e47b836819 100644
--- a/pkg/logging/jsonfile/jsonfile.go
+++ b/pkg/logging/jsonfile/jsonfile.go
@@ -17,7 +17,6 @@
package jsonfile
import (
- "container/ring"
"encoding/json"
"fmt"
"io"
@@ -29,7 +28,7 @@ import (
timetypes "github.com/docker/docker/api/types/time"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
)
// Entry is compatible with Docker "json-file" logs
@@ -54,14 +53,14 @@ func Encode(stdout <-chan string, stderr <-chan string, writer io.Writer) error
e := &Entry{
Stream: name,
}
- for log := range dataChan {
- e.Log = log + "\n"
+ for logEntry := range dataChan {
+ e.Log = logEntry + "\n"
e.Time = time.Now().UTC()
encMu.Lock()
encErr := enc.Encode(e)
encMu.Unlock()
if encErr != nil {
- logrus.WithError(encErr).Errorf("failed to encode JSON")
+ log.L.WithError(encErr).Errorf("failed to encode JSON")
return
}
}
@@ -119,7 +118,7 @@ func writeEntry(e *Entry, stdout, stderr io.Writer, refTime time.Time, timestamp
case "stderr":
writeTo = stderr
default:
- logrus.Errorf("unknown stream name %q, entry=%+v", e.Stream, e)
+ log.L.Errorf("unknown stream name %q, entry=%+v", e.Stream, e)
}
if writeTo != nil {
@@ -129,12 +128,7 @@ func writeEntry(e *Entry, stdout, stderr io.Writer, refTime time.Time, timestamp
return nil
}
-func Decode(stdout, stderr io.Writer, r io.Reader, timestamps bool, since string, until string, tail uint) error {
- var buff *ring.Ring
- if tail != 0 {
- buff = ring.New(int(tail))
- }
-
+func Decode(stdout, stderr io.Writer, r io.Reader, timestamps bool, since string, until string) ([]byte, error) {
dec := json.NewDecoder(r)
now := time.Now()
for {
@@ -142,41 +136,19 @@ func Decode(stdout, stderr io.Writer, r io.Reader, timestamps bool, since string
if err := dec.Decode(&e); err == io.EOF {
break
} else if err != nil {
- return err
- }
-
- if buff == nil {
- // Write out the entry directly
- err := writeEntry(&e, stdout, stderr, now, timestamps, since, until)
+ line, err := io.ReadAll(dec.Buffered())
if err != nil {
- logrus.Errorf("error while writing log entry to output stream: %s", err)
+ return nil, err
}
- } else {
- // Else place the entry in a ring buffer
- buff.Value = &e
- buff = buff.Next()
+ return line, err
}
- }
- if buff != nil {
- // The ring should now contain up to `tail` elements and be set to
- // internally point to the oldest element in the ring.
- buff.Do(func(e interface{}) {
- if e == nil {
- // unallocated ring element
- return
- }
- cast, ok := e.(*Entry)
- if !ok {
- logrus.Errorf("failed to cast Entry struct: %#v", e)
- return
- }
-
- err := writeEntry(cast, stdout, stderr, now, timestamps, since, until)
- if err != nil {
- logrus.Errorf("error while writing log entry to output stream: %s", err)
- }
- })
+ // Write out the entry directly
+ err := writeEntry(&e, stdout, stderr, now, timestamps, since, until)
+ if err != nil {
+ log.L.Errorf("error while writing log entry to output stream: %s", err)
+ }
}
- return nil
+
+ return nil, nil
}
diff --git a/pkg/logging/log_viewer.go b/pkg/logging/log_viewer.go
index 80d0626ef6e..7cbc0292edf 100644
--- a/pkg/logging/log_viewer.go
+++ b/pkg/logging/log_viewer.go
@@ -23,8 +23,9 @@ import (
"os/exec"
"path/filepath"
- "github.com/containerd/nerdctl/pkg/labels/k8slabels"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/labels/k8slabels"
)
// Type alias for functions which write out logs to the provided stdout/stderr Writers.
@@ -37,7 +38,7 @@ var logViewers = make(map[string]LogViewerFunc)
// Registers a LogViewerFunc for the
func RegisterLogViewer(driverName string, lvfn LogViewerFunc) {
if v, ok := logViewers[driverName]; ok {
- logrus.Warnf("A LogViewerFunc with name %q has already been registered: %#v, overriding with %#v either way", driverName, v, lvfn)
+ log.L.Warnf("A LogViewerFunc with name %q has already been registered: %#v, overriding with %#v either way", driverName, v, lvfn)
}
logViewers[driverName] = lvfn
}
@@ -93,7 +94,7 @@ func (lvo *LogViewOptions) Validate() error {
if err != nil {
return err
}
- logrus.Warnf("given relative datastore path %q, transformed it to absolute path: %q", lvo.DatastoreRootPath, abs)
+ log.L.Warnf("given relative datastore path %q, transformed it to absolute path: %q", lvo.DatastoreRootPath, abs)
lvo.DatastoreRootPath = abs
}
@@ -134,6 +135,10 @@ func InitContainerLogViewer(containerLabels map[string]string, lvopts LogViewOpt
return nil, fmt.Errorf("the `cri` log viewer requires nerdctl to be running in experimental mode")
}
+ if lcfg.Driver == "none" {
+ return nil, fmt.Errorf("log type `none` was selected, nothing to log")
+ }
+
lv := &ContainerLogViewer{
loggingConfig: lcfg,
logViewingOptions: lvopts,
diff --git a/pkg/logging/logging.go b/pkg/logging/logging.go
index f81180503f0..0a5e57d5524 100644
--- a/pkg/logging/logging.go
+++ b/pkg/logging/logging.go
@@ -26,11 +26,16 @@ import (
"os"
"path/filepath"
"sort"
+ "strings"
"sync"
+ "time"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/runtime/v2/logging"
- "github.com/sirupsen/logrus"
+ "github.com/fsnotify/fsnotify"
+ "github.com/muesli/cancelreader"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
)
const (
@@ -44,16 +49,16 @@ const (
type Driver interface {
Init(dataStore, ns, id string) error
- PreProcess(dataStore string, config *logging.Config) error
+ PreProcess(ctx context.Context, dataStore string, config *logging.Config) error
Process(stdout <-chan string, stderr <-chan string) error
PostProcess() error
}
-type DriverFactory func(map[string]string) (Driver, error)
-type LogOpsValidateFunc func(logOptMap map[string]string) error
+type DriverFactory func(map[string]string, string) (Driver, error)
+type LogOptsValidateFunc func(logOptMap map[string]string) error
var drivers = make(map[string]DriverFactory)
-var driversLogOptsValidateFunctions = make(map[string]LogOpsValidateFunc)
+var driversLogOptsValidateFunctions = make(map[string]LogOptsValidateFunc)
func ValidateLogOpts(logDriver string, logOpts map[string]string) error {
if value, ok := driversLogOptsValidateFunctions[logDriver]; ok && value != nil {
@@ -62,7 +67,7 @@ func ValidateLogOpts(logDriver string, logOpts map[string]string) error {
return nil
}
-func RegisterDriver(name string, f DriverFactory, validateFunc LogOpsValidateFunc) {
+func RegisterDriver(name string, f DriverFactory, validateFunc LogOptsValidateFunc) {
drivers[name] = f
driversLogOptsValidateFunctions[name] = validateFunc
}
@@ -76,25 +81,28 @@ func Drivers() []string {
return ss
}
-func GetDriver(name string, opts map[string]string) (Driver, error) {
+func GetDriver(name string, opts map[string]string, address string) (Driver, error) {
driverFactory, ok := drivers[name]
if !ok {
return nil, fmt.Errorf("unknown logging driver %q: %w", name, errdefs.ErrNotFound)
}
- return driverFactory(opts)
+ return driverFactory(opts, address)
}
func init() {
- RegisterDriver("json-file", func(opts map[string]string) (Driver, error) {
+ RegisterDriver("none", func(opts map[string]string, address string) (Driver, error) {
+ return &NoneLogger{}, nil
+ }, NoneLogOptsValidate)
+ RegisterDriver("json-file", func(opts map[string]string, address string) (Driver, error) {
return &JSONLogger{Opts: opts}, nil
}, JSONFileLogOptsValidate)
- RegisterDriver("journald", func(opts map[string]string) (Driver, error) {
- return &JournaldLogger{Opts: opts}, nil
+ RegisterDriver("journald", func(opts map[string]string, address string) (Driver, error) {
+ return &JournaldLogger{Opts: opts, Address: address}, nil
}, JournalLogOptsValidate)
- RegisterDriver("fluentd", func(opts map[string]string) (Driver, error) {
+ RegisterDriver("fluentd", func(opts map[string]string, address string) (Driver, error) {
return &FluentdLogger{Opts: opts}, nil
}, FluentdLogOptsValidate)
- RegisterDriver("syslog", func(opts map[string]string) (Driver, error) {
+ RegisterDriver("syslog", func(opts map[string]string, address string) (Driver, error) {
return &SyslogLogger{Opts: opts}, nil
}, SyslogOptsValidate)
}
@@ -113,9 +121,10 @@ func Main(argv2 string) error {
// LogConfig is marshalled as "log-config.json"
type LogConfig struct {
- Driver string `json:"driver"`
- Opts map[string]string `json:"opts,omitempty"`
- LogURI string `json:"-"`
+ Driver string `json:"driver"`
+ Opts map[string]string `json:"opts,omitempty"`
+ LogURI string `json:"-"`
+ Address string `json:"address"`
}
// LogConfigFilePath returns the path of log-config.json
@@ -140,10 +149,25 @@ func LoadLogConfig(dataStore, ns, id string) (LogConfig, error) {
return logConfig, nil
}
-func loggingProcessAdapter(driver Driver, dataStore string, config *logging.Config) error {
- if err := driver.PreProcess(dataStore, config); err != nil {
+func loggingProcessAdapter(ctx context.Context, driver Driver, dataStore string, config *logging.Config) error {
+ if err := driver.PreProcess(ctx, dataStore, config); err != nil {
+ return err
+ }
+
+ stdoutR, err := cancelreader.NewReader(config.Stdout)
+ if err != nil {
+ return err
+ }
+ stderrR, err := cancelreader.NewReader(config.Stderr)
+ if err != nil {
return err
}
+ go func() {
+ <-ctx.Done() // delivered on SIGTERM
+ stdoutR.Cancel()
+ stderrR.Cancel()
+ }()
+
var wg sync.WaitGroup
wg.Add(3)
stdout := make(chan string, 10000)
@@ -151,18 +175,25 @@ func loggingProcessAdapter(driver Driver, dataStore string, config *logging.Conf
processLogFunc := func(reader io.Reader, dataChan chan string) {
defer wg.Done()
defer close(dataChan)
- scanner := bufio.NewScanner(reader)
- for scanner.Scan() {
- if scanner.Err() != nil {
- logrus.Errorf("failed to read log: %v", scanner.Err())
- return
+ r := bufio.NewReader(reader)
+
+ var err error
+
+ for err == nil {
+ var s string
+ s, err = r.ReadString('\n')
+
+ if len(s) > 0 {
+ dataChan <- strings.TrimSuffix(s, "\n")
+ }
+
+ if err != nil && err != io.EOF {
+ log.L.WithError(err).Error("failed to read log")
}
- dataChan <- scanner.Text()
}
}
-
- go processLogFunc(config.Stdout, stdout)
- go processLogFunc(config.Stderr, stderr)
+ go processLogFunc(stdoutR, stdout)
+ go processLogFunc(stderrR, stderr)
go func() {
defer wg.Done()
driver.Process(stdout, stderr)
@@ -175,7 +206,7 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
if dataStore == "" {
return nil, errors.New("got empty data store")
}
- return func(_ context.Context, config *logging.Config, ready func() error) error {
+ return func(ctx context.Context, config *logging.Config, ready func() error) error {
if config.Namespace == "" || config.ID == "" {
return errors.New("got invalid config")
}
@@ -185,7 +216,7 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
if err != nil {
return err
}
- driver, err := GetDriver(logConfig.Driver, logConfig.Opts)
+ driver, err := GetDriver(logConfig.Driver, logConfig.Opts, logConfig.Address)
if err != nil {
return err
}
@@ -193,7 +224,7 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
return err
}
- return loggingProcessAdapter(driver, dataStore, config)
+ return loggingProcessAdapter(ctx, driver, dataStore, config)
} else if !errors.Is(err, os.ErrNotExist) {
// the file does not exist if the container was created with nerdctl < 0.20
return err
@@ -201,3 +232,45 @@ func loggerFunc(dataStore string) (logging.LoggerFunc, error) {
return nil
}, nil
}
+
+func NewLogFileWatcher(dir string) (*fsnotify.Watcher, error) {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return nil, fmt.Errorf("failed to create fsnotify watcher: %v", err)
+ }
+ if err = watcher.Add(dir); err != nil {
+ watcher.Close()
+ return nil, fmt.Errorf("failed to watch directory %q: %w", dir, err)
+ }
+ return watcher, nil
+}
+
+// startTail wait for the next log write.
+// the boolean value indicates if the log file was recreated;
+// the error is error happens during waiting new logs.
+func startTail(ctx context.Context, logName string, w *fsnotify.Watcher) (bool, error) {
+ errRetry := 5
+ for {
+ select {
+ case <-ctx.Done():
+ return false, fmt.Errorf("context cancelled")
+ case e := <-w.Events:
+ switch {
+ case e.Has(fsnotify.Write):
+ return false, nil
+ case e.Has(fsnotify.Create):
+ return filepath.Base(e.Name) == logName, nil
+ default:
+ log.L.Debugf("Received unexpected fsnotify event: %v, retrying", e)
+ }
+ case err := <-w.Errors:
+ log.L.Debugf("Received fsnotify watch error, retrying unless no more retries left, retries: %d, error: %s", errRetry, err)
+ if errRetry == 0 {
+ return false, err
+ }
+ errRetry--
+ case <-time.After(logForceCheckPeriod):
+ return false, nil
+ }
+ }
+}
diff --git a/pkg/logging/logging_test.go b/pkg/logging/logging_test.go
new file mode 100644
index 00000000000..175f1b3e64b
--- /dev/null
+++ b/pkg/logging/logging_test.go
@@ -0,0 +1,115 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package logging
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "math/rand"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+)
+
+type MockDriver struct {
+ processed bool
+ receivedStdout []string
+ receivedStderr []string
+}
+
+func (m *MockDriver) Init(dataStore, ns, id string) error {
+ return nil
+}
+
+func (m *MockDriver) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error {
+ return nil
+}
+
+func (m *MockDriver) Process(stdout <-chan string, stderr <-chan string) error {
+ for line := range stdout {
+ m.receivedStdout = append(m.receivedStdout, line)
+ }
+ for line := range stderr {
+ m.receivedStderr = append(m.receivedStderr, line)
+ }
+ m.processed = true
+ return nil
+}
+
+func (m *MockDriver) PostProcess() error {
+ return nil
+}
+
+func TestLoggingProcessAdapter(t *testing.T) {
+ // Will process a normal String to stdout and a bigger one to stderr
+ normalString := generateRandomString(1024)
+
+ // Generate 64KB of random text of bufio MaxScanTokenSize
+ // https://github.com/containerd/nerdctl/issues/3343
+ hugeString := generateRandomString(bufio.MaxScanTokenSize)
+
+ // Prepare mock driver and logging config
+ driver := &MockDriver{}
+ stdoutBuffer := bytes.NewBufferString(normalString)
+ stderrBuffer := bytes.NewBufferString(hugeString)
+ config := &logging.Config{
+ Stdout: stdoutBuffer,
+ Stderr: stderrBuffer,
+ }
+
+ // Execute the logging process adapter
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ err := loggingProcessAdapter(ctx, driver, "testDataStore", config)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // let bufio read the buffer
+ time.Sleep(50 * time.Millisecond)
+
+ // Verify that the driver methods were called
+ if !driver.processed {
+ t.Fatal("process should be processed")
+ }
+
+ // Verify that the driver received the expected data
+ stdout := strings.Join(driver.receivedStdout, "\n")
+ stderr := strings.Join(driver.receivedStderr, "\n")
+
+ if stdout != normalString {
+ t.Fatalf("stdout is %s, expected %s", stdout, normalString)
+ }
+
+ if stderr != hugeString {
+ t.Fatalf("stderr is %s, expected %s", stderr, hugeString)
+ }
+}
+
+// generateRandomString creates a random string of the given size.
+func generateRandomString(size int) string {
+ characters := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ var sb strings.Builder
+ for i := 0; i < size; i++ {
+ sb.WriteByte(characters[rand.Intn(len(characters))])
+ }
+ return sb.String()
+}
diff --git a/pkg/logging/logs_other.go b/pkg/logging/logs_other.go
new file mode 100644
index 00000000000..94cd53ef2b2
--- /dev/null
+++ b/pkg/logging/logs_other.go
@@ -0,0 +1,33 @@
+//go:build !windows
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+/*
+Forked from https://github.com/kubernetes/kubernetes/blob/cc60b26dee4768e3c5aa0515bbf4ba1824ad38dc/staging/src/k8s.io/cri-client/pkg/logs/logs_other.go
+Copyright The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0
+*/
+package logging
+
+import (
+ "os"
+)
+
+func openFileShareDelete(path string) (*os.File, error) {
+ // Noop. Only relevant for Windows.
+ return os.Open(path)
+}
diff --git a/pkg/logging/logs_windows.go b/pkg/logging/logs_windows.go
new file mode 100644
index 00000000000..c6902d5b52a
--- /dev/null
+++ b/pkg/logging/logs_windows.go
@@ -0,0 +1,53 @@
+//go:build windows
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+/*
+Forked from https://github.com/kubernetes/kubernetes/blob/cc60b26dee4768e3c5aa0515bbf4ba1824ad38dc/staging/src/k8s.io/cri-client/pkg/logs/logs_windows.go
+Copyright The Kubernetes Authors.
+Licensed under the Apache License, Version 2.0
+*/
+package logging
+
+import (
+ "os"
+ "syscall"
+)
+
+// Based on Windows implementation of Windows' syscall.Open
+// https://cs.opensource.google/go/go/+/refs/tags/go1.22.2:src/syscall/syscall_windows.go;l=342
+// In addition to syscall.Open, this function also adds the syscall.FILE_SHARE_DELETE flag to sharemode,
+// which will allow us to read from the file without blocking the file from being deleted or renamed.
+// This is essential for Log Rotation which is done by renaming the open file. Without this, the file rename would fail.
+func openFileShareDelete(path string) (*os.File, error) {
+ pathp, err := syscall.UTF16PtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+
+ var access uint32 = syscall.GENERIC_READ
+ var sharemode uint32 = syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE
+ var createmode uint32 = syscall.OPEN_EXISTING
+ var attrs uint32 = syscall.FILE_ATTRIBUTE_NORMAL
+
+ handle, err := syscall.CreateFile(pathp, access, sharemode, nil, createmode, attrs, 0)
+ if err != nil {
+ return nil, err
+ }
+
+ return os.NewFile(uintptr(handle), path), nil
+}
diff --git a/pkg/logging/none_logger.go b/pkg/logging/none_logger.go
new file mode 100644
index 00000000000..8d316c32465
--- /dev/null
+++ b/pkg/logging/none_logger.go
@@ -0,0 +1,47 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package logging
+
+import (
+ "context"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+)
+
+type NoneLogger struct {
+ Opts map[string]string
+}
+
+func (n *NoneLogger) Init(dataStore, ns, id string) error {
+ return nil
+}
+
+func (n *NoneLogger) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error {
+ return nil
+}
+
+func (n *NoneLogger) Process(stdout <-chan string, stderr <-chan string) error {
+ return nil
+}
+
+func (n *NoneLogger) PostProcess() error {
+ return nil
+}
+
+func NoneLogOptsValidate(_ map[string]string) error {
+ return nil
+}
diff --git a/pkg/logging/none_logger_test.go b/pkg/logging/none_logger_test.go
new file mode 100644
index 00000000000..59e967bc5e1
--- /dev/null
+++ b/pkg/logging/none_logger_test.go
@@ -0,0 +1,74 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package logging
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "gotest.tools/v3/assert"
+
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+)
+
+func TestNoneLogger(t *testing.T) {
+ // Create a temporary directory for potential log files
+ tmpDir := t.TempDir()
+ ctx := context.Background()
+
+ logger := &NoneLogger{
+ Opts: map[string]string{},
+ }
+
+ t.Run("NoLoggingOccurs", func(t *testing.T) {
+ initialFiles, err := os.ReadDir(tmpDir)
+ assert.NilError(t, err, "Failed to read temp dir")
+
+ // Run all logger methods
+ logger.Init(tmpDir, "namespace", "id")
+ logger.PreProcess(ctx, tmpDir, &logging.Config{})
+
+ stdout := make(chan string)
+ stderr := make(chan string)
+
+ go func() {
+ for i := 0; i < 10; i++ {
+ stdout <- "test stdout"
+ stderr <- "test stderr"
+ }
+ close(stdout)
+ close(stderr)
+ }()
+
+ err = logger.Process(stdout, stderr)
+ assert.NilError(t, err, "Process() returned unexpected error")
+
+ logger.PostProcess()
+
+ // Wait a bit to ensure any potential writes would have occurred
+ time.Sleep(100 * time.Millisecond)
+
+ // Check if any new files were created
+ afterFiles, err := os.ReadDir(tmpDir)
+ assert.NilError(t, err, "Failed to read temp dir after operations")
+
+ assert.Equal(t, len(afterFiles), len(initialFiles), "Expected no new files, but directory content changed")
+
+ })
+}
diff --git a/pkg/logging/syslog_logger.go b/pkg/logging/syslog_logger.go
index 16cf62662f0..460caf5313a 100644
--- a/pkg/logging/syslog_logger.go
+++ b/pkg/logging/syslog_logger.go
@@ -17,6 +17,7 @@
package logging
import (
+ "context"
"crypto/tls"
"errors"
"fmt"
@@ -30,9 +31,10 @@ import (
"github.com/docker/go-connections/tlsconfig"
syslog "github.com/yuchanns/srslog"
- "github.com/containerd/containerd/runtime/v2/logging"
- "github.com/containerd/nerdctl/pkg/strutil"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/core/runtime/v2/logging"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
const (
@@ -91,7 +93,7 @@ const (
func SyslogOptsValidate(logOptMap map[string]string) error {
for key := range logOptMap {
if !strutil.InStringSlice(syslogOpts, key) {
- logrus.Warnf("log-opt %s is ignored for syslog log driver", key)
+ log.L.Warnf("log-opt %s is ignored for syslog log driver", key)
}
}
proto, _, err := parseSyslogAddress(logOptMap[syslogAddress])
@@ -121,7 +123,7 @@ func (sy *SyslogLogger) Init(dataStore string, ns string, id string) error {
return nil
}
-func (sy *SyslogLogger) PreProcess(dataStore string, config *logging.Config) error {
+func (sy *SyslogLogger) PreProcess(ctx context.Context, dataStore string, config *logging.Config) error {
logger, err := parseSyslog(config.ID, sy.Opts)
if err != nil {
return err
diff --git a/pkg/mountutil/mountutil.go b/pkg/mountutil/mountutil.go
index 4e44d0bf06a..d55a2cb6646 100644
--- a/pkg/mountutil/mountutil.go
+++ b/pkg/mountutil/mountutil.go
@@ -17,27 +17,30 @@
package mountutil
import (
- "errors"
"fmt"
+ "os"
"path/filepath"
"runtime"
"strings"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/containerd/pkg/userns"
- "github.com/containerd/nerdctl/pkg/idgen"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
- "github.com/containerd/nerdctl/pkg/strutil"
+ "github.com/moby/sys/userns"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/identifiers"
+ "github.com/containerd/nerdctl/v2/pkg/idgen"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+ "github.com/containerd/nerdctl/v2/pkg/strutil"
)
const (
- Bind = "bind"
- Volume = "volume"
- Tmpfs = "tmpfs"
+ Bind = "bind"
+ Volume = "volume"
+ Tmpfs = "tmpfs"
+ Npipe = "npipe"
+ pathSeparator = string(os.PathSeparator)
)
type Processed struct {
@@ -49,79 +52,84 @@ type Processed struct {
Opts []oci.SpecOpts
}
-func ProcessFlagV(s string, volStore volumestore.VolumeStore) (*Processed, error) {
+type volumeSpec struct {
+ Type string
+ Name string
+ Source string
+ AnonymousVolume string
+}
+
+func ProcessFlagV(s string, volStore volumestore.VolumeStore, createDir bool) (*Processed, error) {
var (
- res Processed
+ res *Processed
+ volSpec volumeSpec
src, dst string
options []string
)
- s = strings.TrimLeft(s, ":")
- split := strings.Split(s, ":")
+ split, err := splitVolumeSpec(s)
+ if err != nil {
+ return nil, fmt.Errorf("failed to split volume mount specification: %v", err)
+ }
+
switch len(split) {
case 1:
- dst = s
- res.AnonymousVolume = idgen.GenerateID()
- logrus.Debugf("creating anonymous volume %q, for %q", res.AnonymousVolume, s)
- anonVol, err := volStore.Create(res.AnonymousVolume, []string{})
+ // validate destination
+ dst = split[0]
+ if _, err := validateAnonymousVolumeDestination(dst); err != nil {
+ return nil, err
+ }
+
+ // create anonymous volume
+ volSpec, err = handleAnonymousVolumes(dst, volStore)
if err != nil {
- return nil, fmt.Errorf("failed to create an anonymous volume %q: %w", res.AnonymousVolume, err)
+ return nil, err
+ }
+
+ src = volSpec.Source
+ res = &Processed{
+ Type: volSpec.Type,
+ AnonymousVolume: volSpec.AnonymousVolume,
}
- src = anonVol.Mountpoint
- res.Type = Volume
case 2, 3:
- res.Type = Bind
- src, dst = split[0], split[1]
- if !strings.Contains(src, "/") {
- // assume src is a volume name
- res.Name = src
- vol, err := volStore.Get(src, false)
- if err != nil {
- if errors.Is(err, errdefs.ErrNotFound) {
- vol, err = volStore.Create(src, nil)
- if err != nil {
- return nil, fmt.Errorf("failed to create volume %q: %w", src, err)
- }
- } else {
- return nil, fmt.Errorf("failed to get volume %q: %w", src, err)
- }
- }
- // src is now full path
- src = vol.Mountpoint
- res.Type = Volume
+ // Vaildate destination
+ dst = split[1]
+ dst = strings.TrimLeft(dst, ":")
+ if _, err := isValidPath(dst); err != nil {
+ return nil, err
}
- if !filepath.IsAbs(src) {
- logrus.Warnf("expected an absolute path, got a relative path %q (allowed for nerdctl, but disallowed for Docker, so unrecommended)", src)
- var err error
- src, err = filepath.Abs(src)
- if err != nil {
- return nil, fmt.Errorf("failed to get the absolute path of %q: %w", src, err)
- }
+
+ // Get volume spec
+ src = split[0]
+ volSpec, err = handleVolumeToMount(src, dst, volStore, createDir)
+ if err != nil {
+ return nil, err
}
- if !filepath.IsAbs(dst) {
- return nil, fmt.Errorf("expected an absolute path, got %q", dst)
+
+ src = volSpec.Source
+ res = &Processed{
+ Type: volSpec.Type,
+ Name: volSpec.Name,
+ AnonymousVolume: volSpec.AnonymousVolume,
}
- rawOpts := ""
+
+ // Parse volume options
if len(split) == 3 {
- rawOpts = split[2]
- }
- res.Mode = rawOpts
+ res.Mode = split[2]
- // always call parseVolumeOptions for bind mount to allow the parser to add some default options
- var err error
- var specOpts []oci.SpecOpts
- options, specOpts, err = parseVolumeOptions(res.Type, src, rawOpts)
- if err != nil {
- return nil, fmt.Errorf("failed to parse volume options (%q, %q, %q): %w", res.Type, src, rawOpts, err)
+ rawOpts := res.Mode
+
+ options, res.Opts, err = getVolumeOptions(src, res.Type, rawOpts)
+ if err != nil {
+ return nil, err
+ }
}
- res.Opts = append(res.Opts, specOpts...)
default:
return nil, fmt.Errorf("failed to parse %q", s)
}
- fstype := "nullfs"
+ fstype := DefaultMountType
if runtime.GOOS != "freebsd" {
- fstype = "none"
found := false
for _, opt := range options {
switch opt {
@@ -139,8 +147,8 @@ func ProcessFlagV(s string, volStore volumestore.VolumeStore) (*Processed, error
}
res.Mount = specs.Mount{
Type: fstype,
- Source: src,
- Destination: dst,
+ Source: cleanMount(src),
+ Destination: cleanMount(dst),
Options: options,
}
if userns.RunningInUserNS() {
@@ -150,5 +158,110 @@ func ProcessFlagV(s string, volStore volumestore.VolumeStore) (*Processed, error
}
res.Mount.Options = strutil.DedupeStrSlice(append(res.Mount.Options, unpriv...))
}
- return &res, nil
+
+ return res, nil
+}
+
+func handleBindMounts(source string, createDir bool) (volumeSpec, error) {
+ var res volumeSpec
+ res.Type = Bind
+ res.Source = source
+
+ // Handle relative paths
+ if !filepath.IsAbs(source) {
+ absPath, err := filepath.Abs(source)
+ if err != nil {
+ return res, fmt.Errorf("failed to get the absolute path of %q: %w", source, err)
+ }
+ res.Source = absPath
+ }
+
+ // Create dir if it does not exist
+ if err := createDirOnHost(source, createDir); err != nil {
+ return res, err
+ }
+
+ return res, nil
+}
+
+func handleAnonymousVolumes(s string, volStore volumestore.VolumeStore) (volumeSpec, error) {
+ var res volumeSpec
+ res.AnonymousVolume = idgen.GenerateID()
+
+ log.L.Debugf("creating anonymous volume %q, for %q", res.AnonymousVolume, s)
+ anonVol, err := volStore.CreateWithoutLock(res.AnonymousVolume, []string{})
+ if err != nil {
+ return res, fmt.Errorf("failed to create an anonymous volume %q: %w", res.AnonymousVolume, err)
+ }
+
+ res.Type = Volume
+ res.Source = anonVol.Mountpoint
+ return res, nil
+}
+
+func handleNamedVolumes(source string, volStore volumestore.VolumeStore) (volumeSpec, error) {
+ var res volumeSpec
+ res.Name = source
+
+ // Create returns an existing volume or creates a new one if necessary.
+ vol, err := volStore.CreateWithoutLock(res.Name, nil)
+ if err != nil {
+ return res, fmt.Errorf("failed to get volume %q: %w", res.Name, err)
+ }
+ // src is now an absolute path
+ res.Type = Volume
+ res.Source = vol.Mountpoint
+
+ return res, nil
+}
+
+func getVolumeOptions(src string, vType string, rawOpts string) ([]string, []oci.SpecOpts, error) {
+ // always call parseVolumeOptions for bind mount to allow the parser to add some default options
+ var err error
+ var specOpts []oci.SpecOpts
+ options, specOpts, err := parseVolumeOptions(vType, src, rawOpts)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to parse volume options (%q, %q, %q): %w", vType, src, rawOpts, err)
+ }
+
+ specOpts = append(specOpts, specOpts...)
+ return options, specOpts, nil
+}
+
+func createDirOnHost(src string, createDir bool) error {
+ _, err := os.Stat(src)
+ if err == nil {
+ return nil
+ }
+
+ if !createDir {
+
+ /**
+ * In pkg\mountutil\mountutil_linux.go:432, we disallow creating directories on host if not found
+ * The user gets an error if the directory does not exist:
+ * error mounting "/foo" to rootfs at "/foo": stat /foo: no such file or directory: unknown.
+ * We log this error to give the user a hint that they may need to create the directory on the host.
+ * https://docs.docker.com/storage/bind-mounts/
+ */
+ if os.IsNotExist(err) {
+ log.L.Warnf("mount source %q does not exist. Please make sure to create the directory on the host.", src)
+ return nil
+ }
+ return fmt.Errorf("failed to stat %q: %w", src, err)
+ }
+
+ if !os.IsNotExist(err) {
+ return fmt.Errorf("failed to stat %q: %w", src, err)
+ }
+ if err := os.MkdirAll(src, 0o755); err != nil {
+ return fmt.Errorf("failed to mkdir %q: %w", src, err)
+ }
+ return nil
+}
+
+func isNamedVolume(s string) bool {
+ err := identifiers.ValidateDockerCompat(s)
+
+ // If the volume name is invalid, we assume it is a path
+ return err == nil
}
diff --git a/pkg/mountutil/mountutil_freebsd.go b/pkg/mountutil/mountutil_freebsd.go
index 76766d6a3db..58b32075b82 100644
--- a/pkg/mountutil/mountutil_freebsd.go
+++ b/pkg/mountutil/mountutil_freebsd.go
@@ -20,10 +20,18 @@ import (
"fmt"
"strings"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+)
+
+const (
+ DefaultMountType = "nullfs"
+
+ // FreeBSD doesn't support bind mounts.
+ DefaultPropagationMode = ""
)
func UnprivilegedMountFlags(path string) ([]string, error) {
@@ -31,9 +39,6 @@ func UnprivilegedMountFlags(path string) ([]string, error) {
return m, nil
}
-// FreeBSD doesn't support bind mounts.
-const DefaultPropagationMode = ""
-
// parseVolumeOptions parses specified optsRaw with using information of
// the volume type and the src directory when necessary.
func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
@@ -47,7 +52,7 @@ func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, e
case "":
// NOP
default:
- logrus.Warnf("unsupported volume option %q", opt)
+ log.L.Warnf("unsupported volume option %q", opt)
}
}
var opts []string
diff --git a/pkg/mountutil/mountutil_linux.go b/pkg/mountutil/mountutil_linux.go
index 68940ab9a4e..a6a79d8e963 100644
--- a/pkg/mountutil/mountutil_linux.go
+++ b/pkg/mountutil/mountutil_linux.go
@@ -25,15 +25,17 @@ import (
"strconv"
"strings"
- "github.com/containerd/containerd/containers"
- "github.com/containerd/containerd/mount"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
"github.com/docker/go-units"
mobymount "github.com/moby/sys/mount"
"github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
+
+ "github.com/containerd/containerd/v2/core/containers"
+ "github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
)
/*
@@ -44,6 +46,15 @@ import (
NOTICE: https://github.com/moby/moby/blob/v20.10.5/NOTICE
*/
+const (
+ DefaultMountType = "none"
+
+ // DefaultPropagationMode is the default propagation of mounts
+ // where user doesn't specify mount propagation explicitly.
+ // See also: https://github.com/moby/moby/blob/v20.10.7/volume/mounts/linux_parser.go#L145
+ DefaultPropagationMode = "rprivate"
+)
+
// UnprivilegedMountFlags is from https://github.com/moby/moby/blob/v20.10.5/daemon/oci_linux.go#L420-L450
//
// Get the set of mount flags that are set on the mount that contains the given
@@ -78,11 +89,6 @@ func UnprivilegedMountFlags(path string) ([]string, error) {
return flags, nil
}
-// DefaultPropagationMode is the default propagation of mounts
-// where user doesn't specify mount propagation explicitly.
-// See also: https://github.com/moby/moby/blob/v20.10.7/volume/mounts/linux_parser.go#L145
-const DefaultPropagationMode = "rprivate"
-
// parseVolumeOptions parses specified optsRaw with using information of
// the volume type and the src directory when necessary.
func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
@@ -118,7 +124,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun
case "":
// NOP
default:
- logrus.Warnf("unsupported volume option %q", opt)
+ log.L.Warnf("unsupported volume option %q", opt)
}
}
@@ -144,7 +150,7 @@ func parseVolumeOptionsWithMountInfo(vType, src, optsRaw string, getMountInfoFun
// Older version of runc just ignores "rro", so we have to add "ro" too, to our best effort.
opts = append(opts, "ro", "rro")
if len(propagationRawOpts) != 1 || propagationRawOpts[0] != "rprivate" {
- logrus.Warn("Mount option \"rro\" should be used in conjunction with \"rprivate\"")
+ log.L.Warn("Mount option \"rro\" should be used in conjunction with \"rprivate\"")
}
case "rw":
// NOP
@@ -418,13 +424,14 @@ func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, e
}
fieldsStr := strings.Join(fields, ":")
- logrus.Debugf("Call legacy %s process, spec: %s ", mountType, fieldsStr)
+ log.L.Debugf("Call legacy %s process, spec: %s ", mountType, fieldsStr)
switch mountType {
case Tmpfs:
return ProcessFlagTmpfs(fieldsStr)
case Volume, Bind:
- return ProcessFlagV(fieldsStr, volStore)
+ // createDir=false for --mount option to disallow creating directories on host if not found
+ return ProcessFlagV(fieldsStr, volStore, false)
}
return nil, fmt.Errorf("invalid mount type '%s' must be a volume/bind/tmpfs", mountType)
}
diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go
index 4ecf1f3f458..80e21542cea 100644
--- a/pkg/mountutil/mountutil_linux_test.go
+++ b/pkg/mountutil/mountutil_linux_test.go
@@ -21,11 +21,12 @@ import (
"strings"
"testing"
- "github.com/containerd/containerd/mount"
- "github.com/containerd/containerd/oci"
"github.com/opencontainers/runtime-spec/specs-go"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
+
+ "github.com/containerd/containerd/v2/core/mount"
+ "github.com/containerd/containerd/v2/pkg/oci"
)
// TestParseVolumeOptions tests volume options are parsed as expected.
@@ -206,3 +207,144 @@ func TestProcessTmpfs(t *testing.T) {
assert.DeepEqual(t, expected, x.Mount.Options)
}
}
+
+func TestProcessFlagV(t *testing.T) {
+ tests := []struct {
+ rawSpec string
+ wants *Processed
+ err string
+ }{
+ // Bind volumes: absolute path
+ {
+ rawSpec: "/mnt/foo:/mnt/foo:ro",
+ wants: &Processed{
+ Type: "bind",
+ Mount: specs.Mount{
+ Type: "none",
+ Destination: `/mnt/foo`,
+ Source: `/mnt/foo`,
+ Options: []string{"ro", "rprivate", "rbind"},
+ }},
+ },
+ // Bind volumes: relative path
+ {
+ rawSpec: `./TestVolume/Path:/mnt/foo`,
+ wants: &Processed{
+ Type: "bind",
+ Mount: specs.Mount{
+ Type: "none",
+ Source: "", // will not check source of relative paths
+ Destination: `/mnt/foo`,
+ Options: []string{"rbind"},
+ }},
+ },
+ // Named volumes
+ {
+ rawSpec: `TestVolume:/mnt/foo`,
+ wants: &Processed{
+ Type: "volume",
+ Name: "TestVolume",
+ Mount: specs.Mount{
+ Type: "none",
+ Source: "", // source of anonymous volume is a generated path, so here will not check it.
+ Destination: `/mnt/foo`,
+ Options: []string{"rbind"},
+ }},
+ },
+ {
+ rawSpec: `/mnt/foo:TestVolume`,
+ err: "expected an absolute path, got \"TestVolume\"",
+ },
+ {
+ rawSpec: `/mnt/foo:./foo`,
+ err: "expected an absolute path, got \"./foo\"",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.rawSpec, func(t *testing.T) {
+ processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, false)
+ if err != nil {
+ assert.Error(t, err, tt.err)
+ return
+ }
+
+ assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
+ assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
+ assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
+ assert.DeepEqual(t, processedVolSpec.Mount.Options, tt.wants.Mount.Options)
+
+ if tt.wants.Name != "" {
+ assert.Equal(t, processedVolSpec.Name, tt.wants.Name)
+ }
+ if tt.wants.Mount.Source != "" {
+ assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
+ }
+ })
+ }
+}
+
+func TestProcessFlagVAnonymousVolumes(t *testing.T) {
+ tests := []struct {
+ rawSpec string
+ wants *Processed
+ err string
+ }{
+ {
+ rawSpec: `/mnt/foo`,
+ wants: &Processed{
+ Type: "volume",
+ Mount: specs.Mount{
+ Type: "none",
+ Source: "", // source of anonymous volume is a generated path, so here will not check it.
+ Destination: `/mnt/foo`,
+ }},
+ },
+ {
+ rawSpec: `./TestVolume/Path`,
+ wants: &Processed{
+ Type: "volume",
+ Mount: specs.Mount{
+ Type: "none",
+ Source: "", // source of anonymous volume is a generated path, so here will not check it.
+ Destination: `TestVolume/Path`, // cleanpath() removes the leading "./". Since we are mocking the os.Stat() call, this is fine.
+ }},
+ },
+ {
+ rawSpec: "TestVolume",
+ wants: &Processed{
+ Type: "volume",
+ Mount: specs.Mount{
+ Type: "none",
+ Source: "", // source of anonymous volume is a generated path, so here will not check it.
+ Destination: "TestVolume",
+ }},
+ },
+ {
+ rawSpec: `/mnt/foo::ro`,
+ err: "expected an absolute path, got \"\"",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.rawSpec, func(t *testing.T) {
+ processedVolSpec, err := ProcessFlagV(tt.rawSpec, mockVolumeStore, true)
+ if err != nil {
+ assert.ErrorContains(t, err, tt.err)
+ return
+ }
+
+ assert.Equal(t, processedVolSpec.Type, tt.wants.Type)
+ assert.Assert(t, processedVolSpec.AnonymousVolume != "")
+ assert.Equal(t, processedVolSpec.Mount.Type, tt.wants.Mount.Type)
+ assert.Equal(t, processedVolSpec.Mount.Destination, tt.wants.Mount.Destination)
+
+ if tt.wants.Mount.Source != "" {
+ assert.Equal(t, processedVolSpec.Mount.Source, tt.wants.Mount.Source)
+ }
+
+ // for anonymous volumes, we want to make sure that the source is not the same as the destination
+ assert.Assert(t, processedVolSpec.Mount.Source != processedVolSpec.Mount.Destination)
+ })
+ }
+}
diff --git a/pkg/mountutil/mountutil_test.go b/pkg/mountutil/mountutil_test.go
new file mode 100644
index 00000000000..85f5ee3bff9
--- /dev/null
+++ b/pkg/mountutil/mountutil_test.go
@@ -0,0 +1,38 @@
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package mountutil
+
+import (
+ "runtime"
+
+ "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+)
+
+type MockVolumeStore struct {
+ volumestore.VolumeStore
+}
+
+func (mv *MockVolumeStore) CreateWithoutLock(name string, labels []string) (*native.Volume, error) {
+ if runtime.GOOS == "windows" {
+ return &native.Volume{Name: "test_volume", Mountpoint: "C:\\test\\directory"}, nil
+ }
+ return &native.Volume{Name: "test_volume", Mountpoint: "/test/volume"}, nil
+}
+
+//nolint:unused
+var mockVolumeStore = &MockVolumeStore{}
diff --git a/pkg/mountutil/mountutil_unix.go b/pkg/mountutil/mountutil_unix.go
new file mode 100644
index 00000000000..5bf7e4d2420
--- /dev/null
+++ b/pkg/mountutil/mountutil_unix.go
@@ -0,0 +1,66 @@
+//go:build unix
+
+/*
+ Copyright The containerd Authors.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package mountutil
+
+import (
+ "fmt"
+ "path/filepath"
+ "strings"
+
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+)
+
+func splitVolumeSpec(s string) ([]string, error) {
+ s = strings.TrimLeft(s, ":")
+ split := strings.Split(s, ":")
+ return split, nil
+}
+
+func handleVolumeToMount(source string, dst string, volStore volumestore.VolumeStore, createDir bool) (volumeSpec, error) {
+ switch {
+ // Handle named volumes
+ case isNamedVolume(source):
+ return handleNamedVolumes(source, volStore)
+
+ // Handle bind volumes (file paths)
+ default:
+ return handleBindMounts(source, createDir)
+ }
+}
+
+func cleanMount(p string) string {
+ return filepath.Clean(p)
+}
+
+func isValidPath(s string) (bool, error) {
+ if filepath.IsAbs(s) {
+ return true, nil
+ }
+
+ return false, fmt.Errorf("expected an absolute path, got %q", s)
+}
+
+/*
+For docker compatibility on non-Windows platforms:
+Docker allows anonymous named volumes, relative paths, and absolute paths
+to be mounted into a container.
+*/
+func validateAnonymousVolumeDestination(s string) (bool, error) {
+ return true, nil
+}
diff --git a/pkg/mountutil/mountutil_windows.go b/pkg/mountutil/mountutil_windows.go
index 3867b8d10fc..e81c072f39d 100644
--- a/pkg/mountutil/mountutil_windows.go
+++ b/pkg/mountutil/mountutil_windows.go
@@ -14,16 +14,37 @@
limitations under the License.
*/
+/*
+ Portions from https://github.com/moby/moby/blob/f5c7673ff8fcbd359f75fb644b1365ca9d20f176/volume/mounts/windows_parser.go#L26
+ Copyright (C) Docker/Moby authors.
+ Licensed under the Apache License, Version 2.0
+ NOTICE: https://github.com/moby/moby/blob/master/NOTICE
+*/
+
package mountutil
import (
"fmt"
+ "path/filepath"
+ "regexp"
"strings"
- "github.com/containerd/containerd/errdefs"
- "github.com/containerd/containerd/oci"
- "github.com/containerd/nerdctl/pkg/mountutil/volumestore"
- "github.com/sirupsen/logrus"
+ "github.com/containerd/containerd/v2/pkg/oci"
+ "github.com/containerd/errdefs"
+ "github.com/containerd/log"
+
+ "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore"
+)
+
+const (
+ // Defaults to an empty string
+ // https://github.com/microsoft/hcsshim/blob/5c75f29c1f5cb4d3498d66228637d07477bcb6a1/internal/hcsoci/resources_wcow.go#L140
+ DefaultMountType = ""
+
+ // DefaultPropagationMode is the default propagation of mounts
+ // where user doesn't specify mount propagation explicitly.
+ // See also: https://github.com/moby/moby/blob/v20.10.7/volume/mounts/windows_parser.go#L440-L442
+ DefaultPropagationMode = ""
)
func UnprivilegedMountFlags(path string) ([]string, error) {
@@ -31,11 +52,6 @@ func UnprivilegedMountFlags(path string) ([]string, error) {
return m, nil
}
-// DefaultPropagationMode is the default propagation of mounts
-// where user doesn't specify mount propagation explicitly.
-// See also: https://github.com/moby/moby/blob/v20.10.7/volume/mounts/windows_parser.go#L440-L442
-const DefaultPropagationMode = ""
-
// parseVolumeOptions parses specified optsRaw with using information of
// the volume type and the src directory when necessary.
func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, error) {
@@ -49,7 +65,7 @@ func parseVolumeOptions(vType, src, optsRaw string) ([]string, []oci.SpecOpts, e
case "":
// NOP
default:
- logrus.Warnf("unsupported volume option %q", opt)
+ log.L.Warnf("unsupported volume option %q", opt)
}
}
var opts []string
@@ -68,3 +84,173 @@ func ProcessFlagTmpfs(s string) (*Processed, error) {
func ProcessFlagMount(s string, volStore volumestore.VolumeStore) (*Processed, error) {
return nil, errdefs.ErrNotImplemented
}
+
+func handleVolumeToMount(source string, dst string, volStore volumestore.VolumeStore, createDir bool) (volumeSpec, error) {
+ // Validate source and destination types
+ if _, err := (validateNamedPipeSpec(source, dst)); err != nil {
+ return volumeSpec{}, err
+ }
+
+ switch {
+ // Handle named volumes
+ case isNamedVolume(source):
+ return handleNamedVolumes(source, volStore)
+
+ // Handle named pipes
+ case isNamedPipe(source):
+ return handleNpipeToMount(source)
+
+ // Handle bind volumes (file paths)
+ default:
+ return handleBindMounts(source, createDir)
+ }
+}
+
+func handleNpipeToMount(source string) (volumeSpec, error) {
+ res := volumeSpec{
+ Type: Npipe,
+ Source: source,
+ }
+ return res, nil
+}
+
+func splitVolumeSpec(raw string) ([]string, error) {
+ raw = strings.TrimSpace(raw)
+ raw = strings.TrimLeft(raw, ":")
+ if raw == "" {
+ return nil, fmt.Errorf("invalid empty volume specification")
+ }
+
+ const (
+ // Root drive or relative paths starting with .
+ rxHostDir = `(?:[a-zA-Z]:|\.)[\/\\]`
+
+ // https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats
+ // Windows UNC paths and DOS device paths (and namde pipes)
+ rxUNC = `(?:\\{2}[a-zA-Z0-9_\-\.\?]+\\{1}[^\\*?"|\r\n]+)\\`
+ rxName = `[^\/\\:*?"<>|\r\n]+`
+
+ rxSource = `((?P