diff --git a/.dockerignore b/.dockerignore index 646cd932d8f..e892fca64ae 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ # artifacts /nerdctl _output +*.gomodjail # golangci-lint /build diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index dbfa167ffe3..300fd4574f5 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -33,6 +33,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 @@ -43,7 +44,7 @@ jobs: # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -60,7 +61,7 @@ jobs: # 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@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 + uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml new file mode 100644 index 00000000000..2bf843e2ab6 --- /dev/null +++ b/.github/workflows/job-build.yml @@ -0,0 +1,99 @@ +# This job just builds nerdctl for the golang versions we support (as a smoke test) +name: job-build + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + go-version: + required: true + type: string + runner: + required: true + type: string + canary: + required: false + default: false + type: boolean + +env: + GOTOOLCHAIN: local + +jobs: + build-all-targets: + name: ${{ format('go {0}', inputs.canary && 'canary' || inputs.go-version ) }} + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + defaults: + run: + shell: bash + + env: + GO_VERSION: ${{ inputs.go-version }} + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - if: ${{ inputs.canary }} + name: "Init (canary): retrieve GO_VERSION" + run: | + . ./hack/github/action-helpers.sh + latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" + printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" + [ "$latest_go" != "" ] || \ + github::log::warning "No canary go" "There is currently no canary go version to test. Steps will not run." + + - if: ${{ env.GO_VERSION != '' }} + name: "Init: install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + - if: ${{ env.GO_VERSION != '' }} + name: "Run: make binaries" + run: | + . ./hack/github/action-helpers.sh + + github::md::table::header "OS" "Arch" "Result" "Time" >> $GITHUB_STEP_SUMMARY + + failure= + + build(){ + local goos="$1" + local goarch="${2:-amd64}" + local goarm="${3:-}" + local result + + github::timer::begin + + GOOS="$goos" GOARCH="$goarch" GOARM="$goarm" make binaries \ + && result="$decorator_success" \ + || { + failure=true + result="$decorator_failure" + } + + [ ! "$goarm" ] || goarch="$goarch/v$goarm" + github::md::table::line "$goos" "$goarch" "$result" "$(github::timer::format <(github::timer::tick))" >> $GITHUB_STEP_SUMMARY + } + + # We officially support these + build linux + build linux arm64 + build windows + build freebsd + # These architectures are not released, but we still verify that we can at least compile + build darwin + build linux arm 6 + build linux loong64 + build linux ppc64le + build linux riscv64 + build linux s390x + + [ ! "$failure" ] || exit 1 diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml new file mode 100644 index 00000000000..8ca82c91506 --- /dev/null +++ b/.github/workflows/job-lint-go.yml @@ -0,0 +1,76 @@ +# This job runs golangci-lint +# Note that technically, `make lint-go-all` would run the linter for all targets, and could be called once, on a single instance. +# The point of running it on a matrix instead, each GOOS separately, is to verify that the tooling itself is working on the target OS. +name: job-lint-go + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + go-version: + required: true + type: string + runner: + required: true + type: string + canary: + required: false + default: false + type: boolean + goos: + required: true + type: string + +env: + GOTOOLCHAIN: local + +jobs: + lint-go: + name: ${{ format('{0}{1}', inputs.goos, inputs.canary && ' (go canary)' || '') }} + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + defaults: + run: + shell: bash + env: + GO_VERSION: ${{ inputs.go-version }} + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - if: ${{ inputs.canary }} + name: "Init (canary): retrieve GO_VERSION" + run: | + latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" + printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" + [ "$latest_go" != "" ] || \ + echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." + + - if: ${{ env.GO_VERSION != '' }} + name: "Init: install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + - if: ${{ env.GO_VERSION != '' }} + name: "Init: install dev-tools" + run: | + echo "::group:: make install-dev-tools" + make install-dev-tools + echo "::endgroup::" + + - if: ${{ env.GO_VERSION != '' }} + name: "Run" + run: | + # On canary, lint for all supported targets + if [ "${{ inputs.canary }}" == "true" ]; then + NO_COLOR=true make lint-go-all + else + NO_COLOR=true GOOS="${{ inputs.goos }}" make lint-go + fi diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml new file mode 100644 index 00000000000..2f012789877 --- /dev/null +++ b/.github/workflows/job-lint-other.yml @@ -0,0 +1,38 @@ +# This job runs any subsidiary linter not part of golangci (shell, yaml, etc) +name: job-lint-other + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + +env: + GOTOOLCHAIN: local + +jobs: + lint-other: + name: "yaml | shell" + timeout-minutes: ${{ inputs.timeout }} + runs-on: ${{ inputs.runner }} + defaults: + run: + shell: bash + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: "Run: yaml" + run: | + make lint-yaml + + - name: "Run: shell" + run: | + make lint-shell diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml new file mode 100644 index 00000000000..a3c840642a5 --- /dev/null +++ b/.github/workflows/job-lint-project.yml @@ -0,0 +1,56 @@ +# This job runs containerd shared project-checks, that verifies licenses, headers, and commits. +# To run locally, you may just use `make lint` instead, that does the same thing +# (albeit `make lint` uses more modern versions). +name: job-lint-project + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + go-version: + required: true + type: string + runner: + required: true + type: string + +env: + GOTOOLCHAIN: local + +jobs: + project: + name: "commits, licenses..." + timeout-minutes: ${{ inputs.timeout }} + runs-on: ${{ inputs.runner }} + defaults: + run: + shell: bash + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 100 + path: src/github.com/containerd/nerdctl + + - name: "Init: install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ inputs.go-version }} + check-latest: true + cache-dependency-path: src/github.com/containerd/nerdctl + + - name: "Run" + uses: containerd/project-checks@d7751f3c375b8fe4a84c02a068184ee4c1f59bc4 # v1.2.2 + with: + working-directory: src/github.com/containerd/nerdctl + repo-access-token: ${{ secrets.GITHUB_TOKEN }} + # go-licenses-ignore is set because go-licenses cannot detect the license of the following package: + # * go-base36: Apache-2.0 OR MIT (https://github.com/multiformats/go-base36/blob/master/LICENSE.md) + # + # The list of the CNCF-approved licenses can be found here: + # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md + go-licenses-ignore: | + github.com/multiformats/go-base36 diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml new file mode 100644 index 00000000000..c4457bae1c7 --- /dev/null +++ b/.github/workflows/job-test-dependencies.yml @@ -0,0 +1,54 @@ +# This job pre-heats the cache for the test image by building all dependencies +name: job-test-dependencies + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + containerd-version: + required: false + default: '' + type: string + +env: + GOTOOLCHAIN: local + +jobs: + # This job builds the dependency target of the test docker image for all supported architectures and cache it in GHA + build-dependencies: + # Note: for whatever reason, you cannot access env.RUNNER_ARCH here + name: "${{ contains(inputs.runner, 'arm') && 'arm64' || 'amd64' }}${{ inputs.containerd-version && format(' | {0}', inputs.containerd-version) || ''}}" + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + defaults: + run: + shell: bash + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: "Init: expose GitHub Runtime variables for gha" + uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + + - name: "Run: build dependencies for the integration test environment image" + run: | + # Cache is sharded per-architecture + arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} + docker buildx create --name with-gha --use + # Honor old containerd if requested + args=() + if [ "${{ inputs.containerd-version }}" != "" ]; then + args=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) + fi + docker buildx build \ + --cache-to type=gha,compression=zstd,mode=max,scope=test-integration-dependencies-"$arch" \ + --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ + --target build-dependencies "${args[@]}" . diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml new file mode 100644 index 00000000000..6c1b9bae492 --- /dev/null +++ b/.github/workflows/job-test-in-container.yml @@ -0,0 +1,176 @@ +# This job runs integration tests inside a container, for all supported variants (ipv6, canary, etc) +# Note that it is linux and nerdctl (+/- gomodjail) only. +name: job-test-in-container + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + canary: + required: false + default: false + type: boolean + target: + required: false + default: '' + type: string + binary: + required: false + default: nerdctl + type: string + containerd-version: + required: false + default: '' + type: string + rootlesskit-version: + required: false + default: '' + type: string + ipv6: + required: false + default: false + type: boolean + +env: + GOTOOLCHAIN: local + +jobs: + test: + name: | + ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} + ${{ inputs.target }} + ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} + ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} + ${{ inputs.ipv6 && ' (ipv6)' || '' }} + ${{ inputs.canary && ' (canary)' || '' }} + ${{ inputs.containerd-version && format(' (ctd: {0})', inputs.containerd-version) || '' }} + ${{ inputs.rootlesskit-version && format(' (rlk: {0})', inputs.rootlesskit-version) || '' }} + timeout-minutes: ${{ inputs.timeout }} + runs-on: ${{ inputs.runner }} + defaults: + run: + shell: bash + + env: + # https://github.com/containerd/nerdctl/issues/622 + # The only case when rootlesskit-version is force-specified is when we downgrade explicitly to v1 + WORKAROUND_ISSUE_622: ${{ inputs.rootlesskit-version }} + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: "Init: expose GitHub Runtime variables for gha" + uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + + - name: "Init: 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 + - if: ${{ inputs.canary }} + name: "Init (canary): prepare updated test image" + run: | + . ./hack/build-integration-canary.sh + canary::build::integration + - if: ${{ ! inputs.canary }} + name: "Init: prepare test image" + run: | + buildargs=() + # If the runner is old, use old ubuntu inside the container as well + [ "${{ contains(inputs.runner, '22.04') }}" != "true" ] || buildargs=(--build-arg UBUNTU_VERSION=22.04) + # Honor if we want old containerd + [ "${{ inputs.containerd-version }}" == "" ] || buildargs+=(--build-arg CONTAINERD_VERSION=${{ inputs.containerd-version }}) + # Honor custom targets and if we want old rootlesskit + target=test-integration + if [ "${{ inputs.target }}" != "rootful" ]; then + target+=-${{ inputs.target }} + if [ "${{ inputs.rootlesskit-version }}" != "" ]; then + buildargs+=(--build-arg ROOTLESSKIT_VERSION=${{ inputs.rootlesskit-version }}) + fi + fi + # Cache is sharded per-architecture + arch=${{ env.RUNNER_ARCH == 'ARM64' && 'arm64' || 'amd64' }} + docker buildx create --name with-gha --use + docker buildx build \ + --output=type=docker \ + --cache-from type=gha,scope=test-integration-dependencies-"$arch" \ + -t "$target" --target "$target" \ + "${buildargs[@]}" \ + . + # Rootful needs to disable snap + - if: ${{ inputs.target == 'rootful' }} + name: "Init: remove snap loopback devices (conflicts with our loopback devices in TestRunDevice)" + run: | + sudo systemctl disable --now snapd.service snapd.socket + sudo apt-get purge -qq snapd + sudo losetup -Dv + sudo losetup -lv + # Rootless on modern ubuntu wants apparmor + - if: ${{ inputs.target != 'rootful' && ! contains(inputs.runner, '22.04') }} + name: "Init: prepare apparmor for rootless + ubuntu 24+" + 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 + # ipv6 wants... ipv6 + - if: ${{ inputs.ipv6 }} + name: "Init: ipv6" + run: | + # Enable ipv4 and ipv6 forwarding + sudo sysctl -w net.ipv6.conf.all.forwarding=1 + sudo sysctl -w net.ipv4.ip_forward=1 + # Enable IPv6 for Docker, and configure docker to use containerd for gha + 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: "Run: integration tests" + run: | + . ./hack/github/action-helpers.sh + github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" + + # IPV6 note: 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. + [ "${{ inputs.target }}" == "rootful" ] \ + && args=(test-integration ./hack/test-integration.sh -test.allow-modify-users=true) \ + || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) + if [ "${{ inputs.ipv6 }}" == true ]; then + docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.only-ipv6 -test.target=${{ inputs.binary }} + else + docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=false -test.target=${{ inputs.binary }} + fi + # FIXME: this NEEDS to go away + - name: "Run: integration tests (flaky)" + run: | + . ./hack/github/action-helpers.sh + github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" + + [ "${{ inputs.target }}" == "rootful" ] \ + && args=(test-integration ./hack/test-integration.sh) \ + || args=(test-integration-${{ inputs.target }} /test-integration-rootless.sh ./hack/test-integration.sh) + if [ "${{ inputs.ipv6 }}" == true ]; then + docker run --network host -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.only-ipv6 -test.target=${{ inputs.binary }} + else + docker run -t --rm --privileged -e GITHUB_STEP_SUMMARY="$GITHUB_STEP_SUMMARY" -v "$GITHUB_STEP_SUMMARY":"$GITHUB_STEP_SUMMARY" -e WORKAROUND_ISSUE_622=${WORKAROUND_ISSUE_622:-} "${args[@]}" -test.only-flaky=true -test.target=${{ inputs.binary }} + fi diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml new file mode 100644 index 00000000000..f760c74780f --- /dev/null +++ b/.github/workflows/job-test-in-host.yml @@ -0,0 +1,210 @@ +# This currently test docker and nerdctl on windows (w/o canary) +# Structure is in to allow testing nerdctl on linux as well, though more work is required to make it functional. +name: job-test-in-host + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + canary: + required: false + default: false + type: boolean + binary: + required: false + default: nerdctl + type: string + go-version: + required: true + type: string + docker-version: + required: true + type: string + containerd-version: + required: true + type: string + containerd-sha: + required: true + type: string + containerd-service-sha: + required: true + type: string + windows-cni-version: + required: true + type: string + linux-cni-version: + required: true + type: string + linux-cni-sha: + required: true + type: string + +env: + GOTOOLCHAIN: local + +jobs: + test: + name: | + ${{ inputs.binary != 'nerdctl' && format('{0} < ', inputs.binary) || '' }} + ${{ contains(inputs.runner, 'ubuntu') && ' linux' || ' windows' }} + ${{ contains(inputs.runner, 'arm') && '(arm)' || '' }} + ${{ contains(inputs.runner, '22.04') && '(old ubuntu)' || '' }} + ${{ inputs.canary && ' (canary)' || '' }} + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + defaults: + run: + shell: bash + + env: + SHOULD_RUN: "yes" + GO_VERSION: ${{ inputs.go-version }} + # Both Docker and nerdctl on linux need rootful right now + WITH_SUDO: ${{ contains(inputs.runner, 'ubuntu') }} + CONTAINERD_VERSION: ${{ inputs.containerd-version }} + CONTAINERD_SHA: ${{ inputs.containerd-sha }} + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - if: ${{ inputs.canary }} + name: "Init (canary): retrieve latest go and containerd" + run: | + latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" + latest_containerd="$(. ./hack/provisioning/version/fetch.sh; github::project::latest "containerd/containerd")" + + [ "$latest_go" == "" ] || \ + printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" + [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ] || { + printf "CONTAINERD_VERSION=%s\n" "${latest_containerd:1}" >> "$GITHUB_ENV" + printf "CONTAINERD_SHA=canary is volatile and I accept the risk\n" >> "$GITHUB_ENV" + } + if [ "$latest_go" == "" ] && [ "${latest_containerd:1}" == "$CONTAINERD_VERSION" ]; then + echo "::warning title=No canary::There is currently no canary versions to test. Steps will not run."; + printf "SHOULD_RUN=no\n" >> "$GITHUB_ENV" + fi + + - if: ${{ env.SHOULD_RUN == 'yes' }} + name: "Init: install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + # XXX RUNNER_OS and generally env is too unreliable + # - if: ${{ env.RUNNER_OS == 'Linux' }} + - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} + name: "Init (linux): prepare host" + run: | + if [ "${{ contains(inputs.binary, 'docker') }}" == true ]; then + echo "::group:: configure cdi for docker" + sudo mkdir -p /etc/docker + sudo jq '.features.cdi = true' /etc/docker/daemon.json | sudo tee /etc/docker/daemon.json.tmp && sudo mv /etc/docker/daemon.json.tmp /etc/docker/daemon.json + echo "::endgroup::" + echo "::group:: downgrade docker to the specific version we want to test (${{ inputs.docker-version }})" + sudo apt-get update -qq + sudo apt-get install -qq ca-certificates curl + sudo install -m 0755 -d /etc/apt/keyrings + sudo cp ./hack/provisioning/gpg/docker /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ + $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + sudo apt-get update -qq + sudo apt-get install -qq --allow-downgrades docker-ce=${{ inputs.docker-version }} docker-ce-cli=${{ inputs.docker-version }} + echo "::endgroup::" + else + # FIXME: this is missing runc (see top level workflow note about the state of this) + echo "::group:: install dependencies" + sudo ./hack/provisioning/linux/containerd.sh uninstall + ./hack/provisioning/linux/containerd.sh rootful "$CONTAINERD_VERSION" "amd64" "$CONTAINERD_SHA" "${{ inputs.containerd-service-sha }}" + sudo ./hack/provisioning/linux/cni.sh uninstall + ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + echo "::endgroup::" + + echo "::group:: build nerctl" + go install ./cmd/nerdctl + echo "$HOME/go/bin" >> "$GITHUB_PATH" + # Since tests are going to run root, we need nerdctl to be in a PATH that will survive `sudo` + sudo cp "$(which nerdctl)" /usr/local/bin + echo "::endgroup::" + fi + + # Register QEMU (tonistiigi/binfmt) + # `--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. + echo "::group:: install binfmt" + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/amd64 + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm64 + docker run --quiet --privileged --rm tonistiigi/binfmt --install linux/arm/v7 + echo "::endgroup::" + + # FIXME: remove expect when we are done removing unbuffer from tests + echo "::group:: installing test dependencies" + sudo apt-get install -qq expect + echo "::endgroup::" + + - if: ${{ contains(inputs.runner, 'windows') && env.SHOULD_RUN == 'yes' }} + name: "Init (windows): prepare host" + env: + ctrdVersion: ${{ env.CONTAINERD_VERSION }} + run: | + # Install WinCNI + echo "::group:: install wincni" + GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh + echo "::endgroup::" + + # Install containerd + echo "::group:: install containerd" + powershell hack/provisioning/windows/containerd.ps1 + echo "::endgroup::" + + # Install nerdctl + echo "::group:: build nerctl" + go install ./cmd/nerdctl + echo "::endgroup::" + + choco install jq + + - if: ${{ env.SHOULD_RUN == 'yes' }} + name: "Init: install dev tools" + run: | + echo "::group:: make install-dev-tools" + make install-dev-tools + echo "::endgroup::" + + # ipv6 is tested only on linux + - if: ${{ contains(inputs.runner, 'ubuntu') && env.SHOULD_RUN == 'yes' }} + name: "Run (linux): integration tests (IPv6)" + run: | + . ./hack/github/action-helpers.sh + github::md::h2 "ipv6" >> "$GITHUB_STEP_SUMMARY" + + ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-ipv6 + + - if: ${{ env.SHOULD_RUN == 'yes' }} + name: "Run: integration tests" + run: | + . ./hack/github/action-helpers.sh + github::md::h2 "non-flaky" >> "$GITHUB_STEP_SUMMARY" + + ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=false + + # FIXME: this must go + - if: ${{ env.SHOULD_RUN == 'yes' }} + name: "Run: integration tests (flaky)" + run: | + . ./hack/github/action-helpers.sh + github::md::h2 "flaky" >> "$GITHUB_STEP_SUMMARY" + + ./hack/test-integration.sh -test.target=${{ inputs.binary }} -test.only-flaky=true diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml new file mode 100644 index 00000000000..22e2f3e9f8b --- /dev/null +++ b/.github/workflows/job-test-in-lima.yml @@ -0,0 +1,118 @@ +# Currently, Lima job test only for EL, though in the future it could be used to also test FreeBSD or other linux-es +name: job-test-in-lima + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + target: + required: true + type: string + guest: + required: true + type: string + +jobs: + test: + name: "${{ inputs.guest }} ${{ inputs.target }}" + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + env: + TARGET: ${{ inputs.target }} + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: "Init: lima" + uses: lima-vm/lima-actions/setup@be564a1408f84557d067b099a475652288074b2e # v1.0.0 + id: lima-actions-setup + + - name: "Init: Cache" + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-actions-setup.outputs.version }} + + - name: "Init: start the guest VM" + run: | + set -eux + # containerd=none is set because the built-in containerd support conflicts with Docker + limactl start \ + --name=default \ + --cpus=4 \ + --memory=12 \ + --containerd=none \ + --set '.mounts=null | .portForwards=[{"guestSocket":"/var/run/docker.sock","hostSocket":"{{.Dir}}/sock/docker.sock"}]' \ + template://${{ inputs.guest }} + + # FIXME: the tests should be directly executed in the VM without nesting Docker inside it + # https://github.com/containerd/nerdctl/issues/3858 + - name: "Init: install dockerd in the guest VM" + run: | + set -eux + lima sudo mkdir -p /etc/systemd/system/docker.socket.d + cat <<-EOF | lima sudo tee /etc/systemd/system/docker.socket.d/override.conf + [Socket] + SocketUser=$(whoami) + EOF + lima sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo + lima sudo dnf -q -y install docker-ce --nobest + lima sudo systemctl enable --now docker + + - name: "Init: configure the host to use dockerd in the guest VM" + run: | + set -eux + sudo systemctl disable --now docker.service docker.socket + export DOCKER_HOST="unix://$(limactl ls --format '{{.Dir}}/sock/docker.sock' default)" + echo "DOCKER_HOST=${DOCKER_HOST}" >>$GITHUB_ENV + docker info + docker version + + - name: "Init: expose GitHub Runtime variables for gha" + uses: crazy-max/ghaction-github-runtime@3cb05d89e1f492524af3d41a1c98c83bc3025124 # v3.1.0 + + - name: "Init: prepare integration tests" + run: | + set -eux + + sudo losetup -Dv + sudo losetup -lv + + [ "$TARGET" = "rootless" ] && TARGET=test-integration-rootless || TARGET=test-integration + docker buildx create --name with-gha --use + docker buildx build \ + --output=type=docker \ + --cache-from type=gha,scope=test-integration-dependencies-amd64 \ + -t test-integration --target "${TARGET}" \ + . + + - name: "Run integration tests" + # Presumably, something is broken with the way docker exposes /dev to the container, as it appears to only + # randomly work. Mounting /dev does workaround the issue. + # This might be due to the old kernel shipped with Alma (4.18), or something else between centos/docker. + run: | + set -eux + if [ "$TARGET" = "rootless" ]; then + echo "rootless" + docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false + else + echo "rootful" + docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false + fi + - name: "Run: integration tests (flaky)" + run: | + set -eux + if [ "$TARGET" = "rootless" ]; then + echo "rootless" + docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true + else + echo "rootful" + docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true + fi diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml new file mode 100644 index 00000000000..2c12637326e --- /dev/null +++ b/.github/workflows/job-test-in-vagrant.yml @@ -0,0 +1,60 @@ +# Right now, this is testing solely FreeBSD, but could be used to test other targets. +# Alternatively, this might get replaced entirely by Lima eventually. +name: job-test-in-vagrant + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + +jobs: + test: + # Will appear as freebsd / 14 in GitHub UI + name: "14" + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + - name: "Init: setup cache" + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: /root/.vagrant.d + key: vagrant + + - name: "Init: 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 + # FIXME: https://github.com/containerd/nerdctl/issues/4163 + cat ./hack/provisioning/gpg/hashicorp | 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 -qq + sudo apt-get install -qq 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 -qq ruby-libvirt + sudo apt-get install -qq --no-install-recommends libxslt-dev libxml2-dev libvirt-dev ruby-bundler ruby-dev zlib1g-dev + # Disable strict dependency enforcement to bypass gem version conflicts during the installation of the vagrant-libvirt plugin. + sudo env VAGRANT_DISABLE_STRICT_DEPENDENCY_ENFORCEMENT=1 vagrant plugin install vagrant-libvirt + + - name: "Init: boot VM" + run: | + ln -sf Vagrantfile.freebsd Vagrantfile + sudo vagrant up --no-tty + + - name: "Run: test-unit" + run: sudo vagrant up --provision-with=test-unit + + - name: "Run: test-integration" + run: sudo vagrant up --provision-with=test-integration diff --git a/.github/workflows/job-test-unit.yml b/.github/workflows/job-test-unit.yml new file mode 100644 index 00000000000..c623b402b2b --- /dev/null +++ b/.github/workflows/job-test-unit.yml @@ -0,0 +1,88 @@ +# Note: freebsd tests are not ran here (see integration instead) +name: job-test-unit + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + go-version: + required: true + type: string + runner: + required: true + type: string + canary: + required: false + default: false + type: boolean + windows-cni-version: + required: true + type: string + linux-cni-version: + required: true + type: string + linux-cni-sha: + required: true + type: string + +env: + GOTOOLCHAIN: local + # Windows fails without this + CGO_ENABLED: 0 + +jobs: + test-unit: + name: ${{ format('{0}{1}', inputs.runner, inputs.canary && ' (go canary)' || '') }} + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + defaults: + run: + shell: bash + + env: + GO_VERSION: ${{ inputs.go-version }} + + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + + # If canary is requested, check for the latest unstable release + - if: ${{ inputs.canary }} + name: "Init (canary): retrieve GO_VERSION" + run: | + latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" + printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" + [ "$latest_go" != "" ] || \ + echo "::warning title=No canary go::There is currently no canary go version to test. Following steps will not run." + + - if: ${{ env.GO_VERSION != '' }} + name: "Init: install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + + # Install CNI + - if: ${{ env.GO_VERSION != '' }} + name: "Init: set up CNI" + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + GOPATH=$(go env GOPATH) WINCNI_VERSION=${{ inputs.windows-cni-version }} ./hack/provisioning/windows/cni.sh + elif [ "$RUNNER_OS" == "Linux" ]; then + ./hack/provisioning/linux/cni.sh install "${{ inputs.linux-cni-version }}" "amd64" "${{ inputs.linux-cni-sha }}" + fi + + - if: ${{ env.GO_VERSION != '' }} + name: "Run" + run: | + make test-unit + + # On linux, also run with root + - if: ${{ env.GO_VERSION != '' && env.RUNNER_OS == 'Linux' }} + name: "Run: with root" + run: | + sudo make test-unit diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index 80c63109381..00000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: lint - -on: - push: - branches: - - main - - 'release/**' - pull_request: - -env: - GO_VERSION: 1.24.x - GOTOOLCHAIN: local - -jobs: - go: - timeout-minutes: 5 - name: "go | ${{ matrix.goos }} | ${{ matrix.canary }}" - runs-on: "${{ matrix.os }}" - defaults: - run: - shell: bash - strategy: - fail-fast: false - 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 - 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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - - name: install required linters and dev-tools - run: | - make install-dev-tools - - name: golangci-lint - run: | - NO_COLOR=true GOOS="${{ matrix.goos }}" make lint-go - 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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - - name: install required linters and dev-tools - run: | - make install-dev-tools - - name: yaml - run: make lint-yaml - - name: shell - run: make lint-shell - - name: go imports ordering - run: | - make lint-imports diff --git a/.github/workflows/project.yml b/.github/workflows/project.yml deleted file mode 100644 index 7a7fce563f0..00000000000 --- a/.github/workflows/project.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: project - -on: - push: - branches: - - main - - 'release/**' - pull_request: - -env: - GOTOOLCHAIN: local - -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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - cache-dependency-path: src/github.com/containerd/nerdctl - - uses: containerd/project-checks@d7751f3c375b8fe4a84c02a068184ee4c1f59bc4 # v1.2.2 - with: - working-directory: src/github.com/containerd/nerdctl - repo-access-token: ${{ secrets.GITHUB_TOKEN }} - # go-licenses-ignore is set because go-licenses cannot detect the license of the following package: - # * go-base36: Apache-2.0 OR MIT (https://github.com/multiformats/go-base36/blob/master/LICENSE.md) - # - # The list of the CNCF-approved licenses can be found here: - # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md - go-licenses-ignore: | - github.com/multiformats/go-base36 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72940cf30f5..b68c6395047 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' - 'test-action-release-*' + pull_request: + paths-ignore: + - '**.md' env: GOTOOLCHAIN: local @@ -13,11 +16,22 @@ jobs: release: runs-on: ubuntu-24.04 timeout-minutes: 40 + # The maximum access is "read" for PRs from public forked repos + # https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token + permissions: + contents: write # for releases + id-token: write # for provenances + attestations: write # for provenances steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 + # FIXME: setup-qemu-action is depended by `gomodjail pack` + - name: "Set up QEMU" + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + - name: "Install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - go-version: 1.24.x + go-version: "1.24" + check-latest: true - name: "Compile binaries" run: make artifacts - name: "SHA256SUMS" @@ -39,7 +53,13 @@ jobs: - - - Release manager: [ADD YOUR NAME HERE] (@[ADD YOUR GITHUB ID HERE]) EOF + - name: "Generate artifact attestation" + uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + with: + subject-path: _output/* - name: "Create release" + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/test-canary.yml b/.github/workflows/test-canary.yml deleted file mode 100644 index 45d25c2372e..00000000000 --- a/.github/workflows/test-canary.yml +++ /dev/null @@ -1,98 +0,0 @@ -# This pipeline purpose is solely meant to run a subset of our test suites against upcoming or unreleased dependencies versions -name: "[flaky, see #3988] 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 -qq 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: 40 - 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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - - run: go install ./cmd/nerdctl - - run: make install-dev-tools - # 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 deleted file mode 100644 index 580a9a2181a..00000000000 --- a/.github/workflows/test-kube.yml +++ /dev/null @@ -1,27 +0,0 @@ -# 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 deleted file mode 100644 index a50b3c8a75c..00000000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,523 +0,0 @@ -name: test - -on: - push: - branches: - - main - - 'release/**' - pull_request: - paths-ignore: - - '**.md' - -env: - GO_VERSION: 1.24.x - GOTOOLCHAIN: local - SHORT_TIMEOUT: 5 - LONG_TIMEOUT: 60 - -jobs: - # 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: v2.0.3 - arch: amd64 - - runner: ubuntu-24.04-arm - containerd: v2.0.3 - arch: arm64 - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - 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: "Build dependencies for the integration test environment image" - run: | - docker buildx create --name with-gha --use - docker buildx build \ - --cache-to type=gha,compression=zstd,mode=max,scope=test-integration-dependencies-${ARCH} \ - --cache-from type=gha,scope=test-integration-dependencies-${ARCH} \ - --target build-dependencies --build-arg CONTAINERD_VERSION=${CONTAINERD_VERSION} . - - test-unit: - # 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: - fail-fast: false - matrix: - include: - - os: windows-2022 - goos: windows - - os: ubuntu-24.04 - goos: linux - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: 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: make test-unit - - test-integration: - needs: build-dependencies - timeout-minutes: 40 - name: rootful | ${{ matrix.containerd }} | ${{ matrix.runner }} - runs-on: "${{ matrix.runner }}" - strategy: - fail-fast: false - matrix: - include: - - ubuntu: 22.04 - containerd: v1.6.36 - runner: "ubuntu-22.04" - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.3 - runner: "ubuntu-24.04" - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.3 - runner: "ubuntu-24.04-arm" - 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=test-integration-dependencies-${ARCH} \ - -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 -qq 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.3 - arch: amd64 - 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: 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 buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=test-integration-dependencies-${ARCH} \ - -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 -qq 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" - # 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: 40 - name: "${{ matrix.target }} | ${{ matrix.containerd }} | ${{ matrix.rootlesskit }} | ${{ matrix.ubuntu }}" - runs-on: "${{ matrix.runner }}" - strategy: - fail-fast: false - matrix: - include: - - ubuntu: 22.04 - containerd: v1.6.36 - rootlesskit: v1.1.1 # Deprecated - target: rootless - runner: "ubuntu-22.04" - arch: amd64 - - ubuntu: 24.04 - containerd: v2.0.3 - rootlesskit: v2.3.4 - target: rootless - arch: amd64 - runner: "ubuntu-24.04" - - ubuntu: 24.04 - containerd: v2.0.3 - rootlesskit: v2.3.4 - target: rootless - arch: arm64 - runner: "ubuntu-24.04-arm" - - ubuntu: 24.04 - containerd: v2.0.3 - rootlesskit: v2.3.4 - target: rootless-port-slirp4netns - arch: amd64 - runner: "ubuntu-24.04" - env: - CONTAINERD_VERSION: "${{ matrix.containerd }}" - ARCH: "${{ matrix.arch }}" - UBUNTU_VERSION: "${{ matrix.ubuntu }}" - ROOTLESSKIT_VERSION: "${{ matrix.rootlesskit }}" - TEST_TARGET: "test-integration-${{ matrix.target }}" - steps: - - 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: | - # `--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 buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=test-integration-dependencies-${ARCH} \ - -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 -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 - - build: - timeout-minutes: 5 - name: "build | ${{ matrix.go-version }}" - runs-on: ubuntu-24.04 - strategy: - fail-fast: false - matrix: - go-version: ["1.23.x", "1.24.x"] - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ matrix.go-version }} - check-latest: true - - name: "build" - run: GO_VERSION="$(echo ${{ matrix.go-version }} | sed -e s/.x//)" make binaries - - test-integration-docker-compatibility: - timeout-minutes: 40 - name: docker - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - - 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: "Prepare integration test environment" - run: | - # FIXME: remove expect when we are done removing unbuffer from tests - sudo apt-get install -qq expect - make install-dev-tools - - name: "Ensure that the integration test suite is compatible with Docker" - 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: 40 - 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@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 - with: - go-version: ${{ env.GO_VERSION }} - check-latest: true - - run: | - go install ./cmd/nerdctl - make install-dev-tools - - 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: 40 - name: FreeBSD - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 - 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 -qq - sudo apt-get install -qq 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 -qq ruby-libvirt - sudo apt-get install -qq --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 - - # EL8 is used for testing compatibility with cgroup v1. - # Do not upgrade this to EL9 (cgroup v2). - test-integration-el8: - timeout-minutes: 60 - name: "[flaky, see #3988] EL8 (cgroup v1)" - strategy: - fail-fast: false - matrix: - mode: ["rootful", "rootless"] - runs-on: ubuntu-24.04 - env: - MODE: ${{ matrix.mode }} - # FIXME: this is only necessary to access the build cache. To remove with build cleanup. - CONTAINERD_VERSION: v2.0.3 - steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - fetch-depth: 1 - - uses: lima-vm/lima-actions/setup@be564a1408f84557d067b099a475652288074b2e # v1.0.0 - id: lima-actions-setup - - uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2 - with: - path: ~/.cache/lima - key: lima-${{ steps.lima-actions-setup.outputs.version }} - - name: "Start the guest VM" - run: | - set -eux - # containerd=none is set because the built-in containerd support conflicts with Docker - limactl start \ - --name=default \ - --cpus=4 \ - --memory=12 \ - --containerd=none \ - --set '.mounts=null | .portForwards=[{"guestSocket":"/var/run/docker.sock","hostSocket":"{{.Dir}}/sock/docker.sock"}]' \ - template://almalinux-8 - # FIXME: the tests should be directly executed in the VM without nesting Docker inside it - # https://github.com/containerd/nerdctl/issues/3858 - - name: "Install dockerd in the guest VM" - run: | - set -eux - lima sudo mkdir -p /etc/systemd/system/docker.socket.d - cat <<-EOF | lima sudo tee /etc/systemd/system/docker.socket.d/override.conf - [Socket] - SocketUser=$(whoami) - EOF - lima sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo - lima sudo dnf -q -y install docker-ce --nobest - lima sudo systemctl enable --now docker - - name: "Configure the host to use dockerd in the guest VM" - run: | - set -eux - sudo systemctl disable --now docker.service docker.socket - export DOCKER_HOST="unix://$(limactl ls --format '{{.Dir}}/sock/docker.sock' default)" - echo "DOCKER_HOST=${DOCKER_HOST}" >>$GITHUB_ENV - docker info - docker version - - name: "Expose GitHub Runtime variables for gha" - uses: crazy-max/ghaction-github-runtime@b3a9207c0e1ef41f4cf215303c976869d0c2c1c4 # v3.0.0 - - name: "Prepare integration tests" - run: | - set -eux - - sudo losetup -Dv - sudo losetup -lv - - TARGET=test-integration - [ "$MODE" = "rootless" ] && TARGET=test-integration-rootless - docker buildx create --name with-gha --use - docker buildx build \ - --output=type=docker \ - --cache-from type=gha,scope=amd64-${CONTAINERD_VERSION} \ - -t test-integration --target "${TARGET}" \ - . - - - name: "Run integration tests" - # Presumably, something is broken with the way docker exposes /dev to the container, as it appears to only - # randomly work. Mounting /dev does workaround the issue. - # This might be due to the old kernel shipped with Alma (4.18), or something else between centos/docker. - run: | - set -eux - [ "$MODE" = "rootless" ] && { - echo "rootless" - docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=false - } || { - echo "rootful" - docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=false - } - - name: "Run integration tests (flaky)" - run: | - set -eux - [ "$MODE" = "rootless" ] && { - echo "rootless" - docker run -t -v /dev:/dev --rm --privileged test-integration /test-integration-rootless.sh ./hack/test-integration.sh -test.only-flaky=true - } || { - echo "rootful" - docker run -t -v /dev:/dev --rm --privileged test-integration ./hack/test-integration.sh -test.only-flaky=true - } diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml new file mode 100644 index 00000000000..85b5c1dd650 --- /dev/null +++ b/.github/workflows/workflow-flaky.yml @@ -0,0 +1,56 @@ +# This workflow puts together all known "flaky" and experimental targets +name: "[flaky, see #3988]" + +on: + push: + branches: + - main + - 'release/**' + pull_request: + paths-ignore: + - '**.md' + +jobs: + test-integration-el: + name: "EL${{ inputs.hack }}" + uses: ./.github/workflows/job-test-in-lima.yml + strategy: + fail-fast: false + # EL8 is used for testing compatibility with cgroup v1. + # Unfortunately, EL8 is hard to debug for M1 users (as Lima+M1+EL8 is not runnable because of page size), + # and it currently shows numerous issues. + # Thus, EL9 is also added as target (for a limited time?) so that we can figure out which issues are EL8 specific, + # and which issues could be reproduced on EL9 as well (which would be easier to debug). + matrix: + guest: ["almalinux-8", "almalinux-9"] + target: ["rootful", "rootless"] + with: + timeout: 60 + runner: ubuntu-24.04 + guest: ${{ matrix.guest }} + target: ${{ matrix.target }} + + test-integration-freebsd: + name: "FreeBSD" + uses: ./.github/workflows/job-test-in-vagrant.yml + with: + timeout: 15 + runner: ubuntu-24.04 + + kube: + name: "kubernetes" + runs-on: ubuntu-24.04 + timeout-minutes: 15 + env: + ROOTFUL: true + steps: + - name: "Init: checkout" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 1 + - name: "Run" + run: | + # FIXME: this should be a bit more elegant to use. + ./hack/provisioning/kube/kind.sh + # See https://github.com/containerd/nerdctl/blob/main/docs/testing/README.md#about-parallelization + 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/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 00000000000..c6d6f6a4e7a --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,80 @@ +name: lint + +on: + push: + branches: + - main + - 'release/**' + pull_request: + +jobs: + # Runs golangci to ensure that: + # 1. the tooling is working on the target platform + # 2. the linter is happy + # 3. for canary (if there is a canary go version), does lint for all supported goos + lint-go: + name: "go${{ inputs.hack }}" + uses: ./.github/workflows/job-lint-go.yml + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + goos: linux + - runner: ubuntu-24.04 + goos: freebsd + - runner: macos-15 + goos: darwin + # FIXME: this is currently failing in a nonsensical way, so, running on linux instead... + # - runner: windows-2022 + - runner: ubuntu-24.04 + goos: windows + # Additionally lint for canary + - runner: ubuntu-24.04 + goos: linux + canary: true + with: + timeout: 5 + go-version: "1.24" + runner: ubuntu-24.04 + # Note: in GitHub yaml world, if `matrix.canary` is undefined, and is passed to `inputs.canary`, the job + # will not run. However, if you test it, it will coerce to `false`, hence: + canary: ${{ matrix.canary && true || false }} + goos: ${{ matrix.goos }} + + # Run common project checks (commits, licenses, etc) + lint-project-checks: + name: "project checks" + uses: ./.github/workflows/job-lint-project.yml + with: + timeout: 5 + go-version: "1.24" + runner: ubuntu-24.04 + + # Lint for shell and yaml files + lint-other: + name: "other" + uses: ./.github/workflows/job-lint-other.yml + with: + timeout: 5 + runner: ubuntu-24.04 + + # Verify we can actually build on all supported platforms, and a bunch of architectures + build-for-go: + name: "build for${{ inputs.hack }}" + uses: ./.github/workflows/job-build.yml + strategy: + fail-fast: false + matrix: + include: + # Build for both old and stable go + - go-version: "1.23" + - go-version: "1.24" + # Additionally build for canary + - go-version: "1.24" + canary: true + with: + timeout: 10 + go-version: ${{ matrix.go-version }} + runner: ubuntu-24.04 + canary: ${{ matrix.canary && true || false }} diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml new file mode 100644 index 00000000000..526d19cabf7 --- /dev/null +++ b/.github/workflows/workflow-test.yml @@ -0,0 +1,149 @@ +name: test + +on: + push: + branches: + - main + - 'release/**' + pull_request: + paths-ignore: + - '**.md' + +jobs: + test-unit: + # Note: inputs.hack is undefined - its purpose is to prevent GitHub Actions from displaying all matrix variants as part of the name. + name: "unit${{ inputs.hack }}" + uses: ./.github/workflows/job-test-unit.yml + strategy: + fail-fast: false + matrix: + # Run on all supported platforms but freebsd + # Additionally run on canary for linux + include: + - runner: "ubuntu-24.04" + - runner: "macos-15" + - runner: "windows-2025" + - runner: "ubuntu-24.04" + canary: true + with: + runner: ${{ matrix.runner }} + canary: ${{ matrix.canary && true || false }} + # Windows routinely go over 5 minutes + timeout: 10 + go-version: 1.24 + windows-cni-version: v0.3.1 + linux-cni-version: v1.7.1 + linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 + + # This job builds the dependency target of the test-image for all supported architectures and cache it in GHA + build-dependencies: + name: "dependencies${{ inputs.hack }}" + uses: ./.github/workflows/job-test-dependencies.yml + strategy: + fail-fast: false + matrix: + include: + # Build for arm & amd, current containerd + - runner: ubuntu-24.04 + - runner: ubuntu-24.04-arm + # Additionally build for old containerd on amd + - runner: ubuntu-24.04 + containerd-version: v1.6.38 + with: + runner: ${{ matrix.runner }} + containerd-version: ${{ matrix.containerd-version }} + timeout: 20 + + test-integration-container: + name: "in-container${{ inputs.hack }}" + uses: ./.github/workflows/job-test-in-container.yml + needs: build-dependencies + strategy: + fail-fast: false + matrix: + include: + ###### Rootless + # amd64 + - runner: ubuntu-24.04 + target: rootless + # arm64 + - runner: ubuntu-24.04-arm + target: rootless + # port-slirp4netns + - runner: ubuntu-24.04 + target: rootless-port-slirp4netns + # old containerd + old ubuntu + old rootlesskit + - runner: ubuntu-22.04 + target: rootless + containerd-version: v1.6.38 + rootlesskit-version: v1.1.1 + # gomodjail + - runner: ubuntu-24.04 + target: rootless + binary: "nerdctl.gomodjail" + ###### Rootful + # amd64 + - runner: ubuntu-24.04 + target: rootful + # arm64 + - runner: ubuntu-24.04-arm + target: rootful + # old containerd + old ubuntu + - runner: ubuntu-22.04 + target: rootful + containerd-version: v1.6.38 + # ipv6 + - runner: ubuntu-24.04 + target: rootful + ipv6: true + # all canary + - runner: ubuntu-24.04 + target: rootful + canary: true + + with: + timeout: 45 + runner: ${{ matrix.runner }} + target: ${{ matrix.target }} + binary: ${{ matrix.binary && matrix.binary || 'nerdctl' }} + containerd-version: ${{ matrix.containerd-version }} + rootlesskit-version: ${{ matrix.rootlesskit-version }} + ipv6: ${{ matrix.ipv6 && true || false }} + canary: ${{ matrix.canary && true || false }} + + test-integration-host: + name: "in-host${{ inputs.hack }}" + uses: ./.github/workflows/job-test-in-host.yml + strategy: + fail-fast: false + matrix: + include: + # Test on windows w/o canary + - runner: windows-2022 + - runner: windows-2025 + canary: true + # Test docker on linux + - runner: ubuntu-24.04 + binary: docker + + # FIXME: running nerdctl on the host is work in progress + # (we miss runc to be installed on the host - and obviously other deps) + # Plan is to pause this for now and first consolidate dependencies management (wrt Dockerfile vs. host-testing CI) + # before we can really start testing linux nerdctl on the host. + # - runner: ubuntu-24.04 + # - runner: ubuntu-24.04 + # canary: true + with: + timeout: 45 + runner: ${{ matrix.runner }} + binary: ${{ matrix.binary != '' && matrix.binary || 'nerdctl' }} + canary: ${{ matrix.canary && true || false }} + go-version: 1.24 + windows-cni-version: v0.3.1 + docker-version: 5:28.0.4-1~ubuntu.24.04~noble + containerd-version: 2.1.1 + # Note: these as for amd64 + containerd-sha: 918e88fd393c28c89424e6535df0546ca36c1dfa7d8a5d685dee70b449380a9b + containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 + linux-cni-version: v1.7.1 + linux-cni-sha: 1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml new file mode 100644 index 00000000000..306ef75d55b --- /dev/null +++ b/.github/workflows/workflow-tigron.yml @@ -0,0 +1,91 @@ +name: tigron + +on: + push: + branches: + - main + - 'release/**' + pull_request: + paths: 'mod/tigron/**' + +env: + GO_VERSION: "1.24" + GOTOOLCHAIN: local + +jobs: + lint: + timeout-minutes: 15 + name: "${{ matrix.goos }} ${{ matrix.runner }} | go ${{ matrix.canary }}" + runs-on: ${{ matrix.runner }} + defaults: + run: + shell: bash + strategy: + matrix: + include: + - runner: ubuntu-24.04 + - runner: macos-15 + - runner: windows-2022 + - runner: ubuntu-24.04 + goos: freebsd + - runner: ubuntu-24.04 + canary: go-canary + steps: + - name: "Checkout project" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 100 + - if: ${{ matrix.canary }} + name: "Init (canary): retrieve GO_VERSION" + run: | + latest_go="$(. ./hack/provisioning/version/fetch.sh; go::canary::for::go-setup)" + printf "GO_VERSION=%s\n" "$latest_go" >> "$GITHUB_ENV" + [ "$latest_go" != "" ] || \ + echo "::warning title=No canary go::There is currently no canary go version to test. Steps will not run." + - if: ${{ env.GO_VERSION != '' }} + name: "Install go" + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ env.GO_VERSION }} + check-latest: true + - if: ${{ env.GO_VERSION != '' }} + name: "Install tools" + run: | + cd mod/tigron + echo "::group:: make install-dev-tools" + make install-dev-tools + if [ "$RUNNER_OS" == macOS ]; then + brew install yamllint shellcheck + fi + echo "::endgroup::" + - if: ${{ env.GO_VERSION != '' && env.RUNNER_OS == 'Linux' && matrix.goos == '' }} + name: "lint" + env: + NO_COLOR: true + run: | + echo "::group:: lint" + cd mod/tigron + export LINT_COMMIT_RANGE="$(jq -r '.after + "..HEAD"' ${GITHUB_EVENT_PATH})" + make lint + echo "::endgroup::" + - if: ${{ env.GO_VERSION != '' }} + name: "test-unit" + run: | + echo "::group:: unit test" + cd mod/tigron + make test-unit + echo "::endgroup::" + - if: ${{ env.GO_VERSION != '' }} + name: "test-unit-race" + run: | + echo "::group:: race test" + cd mod/tigron + make test-unit-race + echo "::endgroup::" + - if: ${{ env.GO_VERSION != '' }} + name: "test-unit-bench" + run: | + echo "::group:: bench" + cd mod/tigron + make test-unit-bench + echo "::endgroup::" diff --git a/.gitignore b/.gitignore index 9381e921112..1078655195f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # artifacts /nerdctl _output +*.gomodjail # golangci-lint /build diff --git a/.golangci.yml b/.golangci.yml index d93e5bbdbbb..7fda716e51a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,167 +1,268 @@ ---- +version: "2" + run: - concurrency: 6 - timeout: 5m + modules-download-mode: readonly + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + linters: - disable-all: true + default: none enable: - - depguard - - gofmt - - goimports + # 1. This is the default enabled set of golanci + + # We should consider enabling errcheck + # - errcheck - govet - ineffassign - - misspell - - nakedret - - prealloc - - typecheck - # - asciicheck - # - bodyclose - # - dogsled - # - dupl - # - errcheck - # - errorlint - # - exhaustive - # - exhaustivestruct - # - exportloopref - # - funlen - # - gci - # - gochecknoglobals - # - gochecknoinits - # - gocognit - # - goconst - # - gocritic - # - gocyclo - # - godot - # - godox - # - goerr113 - # - gofumpt - # - goheader - # - golint - # - gomnd - # - gomodguard - # - goprintffuncname - # - gosec (gas) - - gosimple # (megacheck) - # - interfacer - # - lll - # - maligned - # - nestif - # - nlreturn - # - noctx - # - nolintlint - - revive - # - rowserrcheck - # - scopelint - # - sqlclosecheck - staticcheck - - stylecheck - # - testpackage - # - tparallel - - unconvert - # - unparam - unused - # - whitespace - # - wrapcheck - # - wsl -linters-settings: - gocritic: - enabled-checks: - # Diagnostic - - appendAssign - - argOrder - - badCond - - caseOrder - - codegenComment - - commentedOutCode - - deprecatedComment - - dupArg - - dupBranchBody - - dupCase - - dupSubExpr - - exitAfterDefer - - flagDeref - - flagName - - nilValReturn - - offBy1 - - sloppyReassign - - weakCond - - octalLiteral - - # Performance - - appendCombine - - equalFold - - hugeParam - - indexAlloc - - rangeExprCopy - - rangeValCopy - - # Style - - assignOp - - boolExprSimplify - - captLocal - - commentFormatting - - commentedOutImport - - defaultCaseOrder - - docStub - - elseif - - emptyFallthrough - - emptyStringTest - - hexLiteral - - ifElseChain - - methodExprCall - - regexpMust - - singleCaseSwitch - - sloppyLen - - stringXbytes - - switchTrue - - typeAssertChain - - typeSwitchVar - - underef - - unlabelStmt - - unlambda - - unslice - - valSwap - - wrapperFunc - - yodaStyleExpr - - # Opinionated - - builtinShadow - - importShadow - - initClause - - nestingReduce - - paramTypeCombine - - ptrToRefParam - - typeUnparen - - unnamedResult - - unnecessaryBlock - - depguard: - rules: - # pkg files must not depend on cobra nor anything in cmd - pkg: - files: - - "**/pkg/**/*.go" - deny: - - pkg: "github.com/spf13/cobra" - desc: "pkg must not depend on cobra" - - pkg: "github.com/spf13/pflag" - desc: "pkg must not depend on pflag" - - pkg: "github.com/spf13/viper" - desc: "pkg must not depend on viper" - - pkg: "github.com/containerd/nerdctl/v2/cmd" - desc: "pkg must not depend on any cmd files" - no-patent: - deny: - - pkg: "github.com/hashicorp/golang-lru/arc/v2" - desc: "patented (https://github.com/hashicorp/golang-lru/blob/arc/v2.0.7/arc/arc.go#L18)" -issues: - max-issues-per-linter: 0 - max-same-issues: 0 - exclude-rules: - - linters: - - revive - text: "unused-parameter" + # 2. These are not part of the default set + + # Important to prevent import of certain packages + - depguard + # Removes unnecessary conversions + - unconvert + # Flag common typos + - misspell + # A meta-linter seen as a good replacement for golint + - revive + # Gocritic + - gocritic + + # 3. We used to use these, but have now removed them + + # Use of prealloc is generally premature optimization and performance profiling should be done instead + # https://golangci-lint.run/usage/linters/#prealloc + # - prealloc + # Provided by revive in a better way + # - nakedret + + settings: + staticcheck: + checks: + # Below is the default set + - "all" + - "-ST1000" + - "-ST1003" + - "-ST1016" + - "-ST1020" + - "-ST1021" + - "-ST1022" + + ##### TODO: fix and enable these + # 4 occurrences. + # Use fmt.Fprintf(x, ...) instead of x.Write(fmt.Sprintf(...)) https://staticcheck.dev/docs/checks#QF1012 + - "-QF1012" + # 6 occurrences. + # Apply De Morgan’s law https://staticcheck.dev/docs/checks#QF1001 + - "-QF1001" + # 10 occurrences. + # Convert if/else-if chain to tagged switch https://staticcheck.dev/docs/checks#QF1003 + - "-QF1003" + + ##### These have been vetted to be disabled. + # 55 occurrences. Omit embedded fields from selector expression https://staticcheck.dev/docs/checks#QF1008 + # Usefulness is questionable. + - "-QF1008" -output: - sort-results: true + revive: + enable-all-rules: true + rules: + # See https://revive.run/r + + ##### P0: we should do it ASAP. + - name: max-control-nesting + # 10 occurences (at default 5). Deep nesting hurts readibility. + arguments: [7] + - name: deep-exit + # 11 occurrences. Do not exit in random places. + disabled: true + - name: unchecked-type-assertion + # 14 occurrences. This is generally risky and encourages bad coding for newcomers. + disabled: true + - name: bare-return + # 31 occurrences. Bare returns are just evil, very unfriendly, and make reading and editing much harder. + disabled: true + - name: import-shadowing + # 44 occurrences. Shadowing makes things prone to errors / confusing to read. + disabled: true + - name: use-errors-new + # 84 occurrences. Improves error testing. + disabled: true + + ##### P1: consider making a dent on these, but not critical. + - name: argument-limit + # 4 occurrences (at default 8). Long windy arguments list for functions are hard to read. Use structs instead. + arguments: [12] + - name: unnecessary-stmt + # 5 occurrences. Increase readability. + disabled: true + - name: defer + # 7 occurrences. Confusing to read for newbies. + disabled: true + - name: confusing-naming + # 10 occurrences. Hurts readability. + disabled: true + - name: early-return + # 10 occurrences. Would improve readability. + disabled: true + - name: function-result-limit + # 12 occurrences (at default 3). A function returning many results is probably too big. + arguments: [7] + - name: function-length + # 155 occurrences (at default 0, 75). Really long functions should really be broken up in most cases. + arguments: [0, 450] + - name: cyclomatic + # 204 occurrences (at default 10) + arguments: [100] + - name: unhandled-error + # 222 occurrences. Could indicate failure to handle broken conditions. + disabled: true + - name: cognitive-complexity + arguments: [197] + # 441 occurrences (at default 7). We should try to lower it (involves significant refactoring). + + ##### P2: nice to have. + - name: max-public-structs + # 7 occurrences (at default 5). Might indicate overcrowding of public API. + arguments: [25] + - name: confusing-results + # 13 occurrences. Have named returns when the type stutters. + # Makes it a bit easier to figure out function behavior just looking at signature. + disabled: true + - name: comment-spacings + # 50 occurrences. Makes code look less wonky / ease readability. + disabled: true + - name: use-any + # 30 occurrences. `any` instead of `interface{}`. Cosmetic. + disabled: true + - name: empty-lines + # 85 occurrences. Makes code look less wonky / ease readability. + disabled: true + - name: package-comments + # 100 occurrences. Better for documentation... + disabled: true + - name: exported + # 577 occurrences. Forces documentation of any exported symbol. + disabled: true + + ###### Permanently disabled. Below have been reviewed and vetted to be unnecessary. + - name: line-length-limit + # Formatter `golines` takes care of this. + disabled: true + - name: nested-structs + # 5 occurrences. Trivial. This is not that hard to read. + disabled: true + - name: flag-parameter + # 52 occurrences. Not sure if this is valuable. + disabled: true + - name: unused-parameter + # 505 occurrences. A lot of work for a marginal improvement. + disabled: true + - name: unused-receiver + # 31 occurrences. Ibid. + disabled: true + - name: add-constant + # 2605 occurrences. Kind of useful in itself, but unacceptable amount of effort to fix + disabled: true + + depguard: + rules: + no-patent: + # do not link in golang-lru anywhere (problematic patent) + deny: + - pkg: github.com/hashicorp/golang-lru/arc/v2 + desc: patented (https://github.com/hashicorp/golang-lru/blob/arc/v2.0.7/arc/arc.go#L18) + pkg: + # pkg files must not depend on cobra nor anything in cmd + files: + - '**/pkg/**/*.go' + deny: + - pkg: github.com/spf13/cobra + desc: pkg must not depend on cobra + - pkg: github.com/spf13/pflag + desc: pkg must not depend on pflag + - pkg: github.com/spf13/viper + desc: pkg must not depend on viper + - pkg: github.com/containerd/nerdctl/v2/cmd + desc: pkg must not depend on any cmd files + gocritic: + disabled-checks: + # Below are normally enabled by default, but we do not pass + - appendAssign + - ifElseChain + - unslice + - badCall + - assignOp + - commentFormatting + - captLocal + - singleCaseSwitch + - wrapperFunc + - elseif + - regexpMust + enabled-checks: + # Below used to be enabled, but we do not pass anymore + # - paramTypeCombine + # - octalLiteral + # - unnamedResult + # - equalFold + # - sloppyReassign + # - emptyStringTest + # - hugeParam + # - appendCombine + # - stringXbytes + # - ptrToRefParam + # - commentedOutCode + # - rangeValCopy + # - methodExprCall + # - yodaStyleExpr + # - typeUnparen + + # We enabled these and we pass + - nilValReturn + - weakCond + - indexAlloc + - rangeExprCopy + - boolExprSimplify + - commentedOutImport + - docStub + - emptyFallthrough + - hexLiteral + - typeAssertChain + - unlabelStmt + - builtinShadow + - importShadow + - initClause + - nestingReduce + - unnecessaryBlock + exclusions: + generated: disable + +formatters: + settings: + gci: + sections: + - standard + - default + - prefix(github.com/containerd) + - localmodule + no-inline-comments: true + no-prefix-comments: true + custom-order: true + gofumpt: + extra-rules: true + golines: + max-len: 500 + tab-len: 4 + shorten-comments: true + enable: + - gci + - gofmt + # We might consider enabling the following: + # - gofumpt + - golines + exclusions: + generated: disable diff --git a/Dockerfile b/Dockerfile index 0d441c18acd..ef3b8d55282 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,49 +15,53 @@ # ----------------------------------------------------------------------------- # Usage: `docker run -it --privileged `. Make sure to add `-t` and `--privileged`. -# TODO: verify commit hash - # Basic deps -ARG CONTAINERD_VERSION=v2.0.3 -ARG RUNC_VERSION=v1.2.5 -ARG CNI_PLUGINS_VERSION=v1.6.2 +# @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- +ARG CONTAINERD_VERSION=v2.1.1@cb1076646aa3740577fafbf3d914198b7fe8e3f7 +ARG RUNC_VERSION=v1.3.0@4ca628d1d4c974f92d24daccb901aa078aad748e +ARG CNI_PLUGINS_VERSION=v1.7.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.20.1 +ARG BUILDKIT_VERSION=v0.21.1@BINARY # Extra deps: Lazy-pulling -ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3 +ARG STARGZ_SNAPSHOTTER_VERSION=v0.16.3@BINARY # Extra deps: Encryption -ARG IMGCRYPT_VERSION=v2.0.0 +ARG IMGCRYPT_VERSION=v2.0.1@c377ec98ff79ec9205eabf555ebd2ea784738c6c # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v2.3.4 -ARG SLIRP4NETNS_VERSION=v1.3.1 +ARG ROOTLESSKIT_VERSION=v2.3.5@BINARY +ARG SLIRP4NETNS_VERSION=v1.3.2@BINARY # Extra deps: bypass4netns -ARG BYPASS4NETNS_VERSION=v0.4.2 +ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS -ARG FUSE_OVERLAYFS_VERSION=v1.14 -ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.1 +ARG FUSE_OVERLAYFS_VERSION=v1.15@BINARY +ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.6@BINARY # Extra deps: Init -ARG TINI_VERSION=v0.19.0 +ARG TINI_VERSION=v0.19.0@BINARY # Extra deps: Debug -ARG BUILDG_VERSION=v0.4.1 +ARG BUILDG_VERSION=v0.5.3@BINARY +# Extra deps: gomodjail +ARG GOMODJAIL_VERSION=v0.1.2@0a86b34442a491fa8f5e4565e9c846fce310239c # Test deps +# Currently, the Docker Official Images and the test deps are not pinned by the hash ARG GO_VERSION=1.24 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.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 +ARG GOTESTSUM_VERSION=v1.12.2 +ARG NYDUS_VERSION=v2.3.1 +ARG SOCI_SNAPSHOTTER_VERSION=0.9.0 +ARG KUBO_VERSION=v0.34.1 -FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1 AS xx +FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.6.1@sha256:923441d7c25f1e2eb5789f82d987693c47b8ed987c4ab3b075d6ed2b5d6779a3 AS xx -FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base-debian +FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-bookworm AS build-base COPY --from=xx / / ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ + make \ git \ + curl \ dpkg-dev ARG TARGETARCH # libbtrfs: for containerd @@ -70,56 +74,63 @@ RUN xx-apt-get update -qq && xx-apt-get install -qq --no-install-recommends \ libseccomp-dev \ pkg-config RUN git config --global advice.detachedHead false +ADD hack/git-checkout-tag-with-hash.sh /usr/local/bin/ -FROM build-base-debian AS build-containerd +FROM build-base AS build-containerd ARG TARGETARCH ARG CONTAINERD_VERSION -RUN git clone https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd +RUN git clone --quiet --depth 1 --branch "${CONTAINERD_VERSION%%@*}" https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd WORKDIR /go/src/github.com/containerd/containerd -RUN git checkout ${CONTAINERD_VERSION} && \ +RUN git-checkout-tag-with-hash.sh ${CONTAINERD_VERSION} && \ mkdir -p /out /out/$TARGETARCH && \ cp -a containerd.service /out 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 +FROM build-base AS build-runc ARG RUNC_VERSION ARG TARGETARCH -RUN git clone https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc +RUN git clone --quiet --depth 1 --branch "${RUNC_VERSION%%@*}" https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc WORKDIR /go/src/github.com/opencontainers/runc -RUN git checkout ${RUNC_VERSION} && \ +RUN git-checkout-tag-with-hash.sh ${RUNC_VERSION} && \ mkdir -p /out ENV CGO_ENABLED=1 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 +FROM build-base AS build-bypass4netns ARG BYPASS4NETNS_VERSION ARG TARGETARCH -RUN git clone https://github.com/rootless-containers/bypass4netns.git /go/src/github.com/rootless-containers/bypass4netns +RUN git clone --quiet --depth 1 --branch "${BYPASS4NETNS_VERSION%%@*}" https://github.com/rootless-containers/bypass4netns.git /go/src/github.com/rootless-containers/bypass4netns WORKDIR /go/src/github.com/rootless-containers/bypass4netns -RUN git checkout ${BYPASS4NETNS_VERSION} && \ +RUN git-checkout-tag-with-hash.sh ${BYPASS4NETNS_VERSION} && \ mkdir -p /out/${TARGETARCH} 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 +FROM build-base AS build-gomodjail +ARG GOMODJAIL_VERSION +ARG TARGETARCH +RUN git clone --quiet --depth 1 --branch "${GOMODJAIL_VERSION%%@*}" https://github.com/AkihiroSuda/gomodjail.git /go/src/github.com/AkihiroSuda/gomodjail +WORKDIR /go/src/github.com/AkihiroSuda/gomodjail +RUN git-checkout-tag-with-hash.sh ${GOMODJAIL_VERSION} && \ + mkdir -p /out/${TARGETARCH} +RUN GO=xx-go make STATIC=1 && \ + xx-verify --static _output/bin/gomodjail && cp -a _output/bin/gomodjail /out/${TARGETARCH} + +FROM build-base AS build-kubo ARG KUBO_VERSION ARG TARGETARCH -RUN git clone https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo +RUN git clone --quiet --depth 1 --branch "${KUBO_VERSION%%@*}" https://github.com/ipfs/kubo.git /go/src/github.com/ipfs/kubo WORKDIR /go/src/github.com/ipfs/kubo -RUN git checkout ${KUBO_VERSION} && \ +RUN git-checkout-tag-with-hash.sh ${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 -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 @@ -134,12 +145,13 @@ RUN mkdir -p /out/share/doc/nerdctl-full && touch /out/share/doc/nerdctl-full/RE 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 -RUN echo "- containerd: ${CONTAINERD_VERSION}" >> /out/share/doc/nerdctl-full/README.md +RUN echo "- containerd: ${CONTAINERD_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG RUNC_VERSION COPY --from=build-runc /out/runc.${TARGETARCH:-amd64} /out/bin/runc -RUN echo "- runc: ${RUNC_VERSION}" >> /out/share/doc/nerdctl-full/README.md +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" && \ +RUN CNI_PLUGINS_VERSION=${CNI_PLUGINS_VERSION%%@*}; \ + fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VERSION}.tgz" && \ 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 && \ @@ -147,7 +159,8 @@ RUN fname="cni-plugins-${TARGETOS:-linux}-${TARGETARCH:-amd64}-${CNI_PLUGINS_VER rm -f "${fname}" && \ 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" && \ +RUN BUILDKIT_VERSION=${BUILDKIT_VERSION%%@*}; \ + fname="buildkit-${BUILDKIT_VERSION}.${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ 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 && \ @@ -161,7 +174,8 @@ RUN cd /out/lib/systemd/system && \ echo "" >> buildkit.service && \ 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" && \ +RUN STARGZ_SNAPSHOTTER_VERSION=${STARGZ_SNAPSHOTTER_VERSION%%@*}; \ + fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ 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 - && \ @@ -171,13 +185,14 @@ RUN fname="stargz-snapshotter-${STARGZ_SNAPSHOTTER_VERSION}-${TARGETOS:-linux}-$ mv stargz-snapshotter.service /out/lib/systemd/system/stargz-snapshotter.service && \ echo "- Stargz Snapshotter: ${STARGZ_SNAPSHOTTER_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG IMGCRYPT_VERSION -RUN git clone https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \ +RUN git clone --quiet --depth 1 --branch "${IMGCRYPT_VERSION%%@*}" https://github.com/containerd/imgcrypt.git /go/src/github.com/containerd/imgcrypt && \ cd /go/src/github.com/containerd/imgcrypt && \ - git checkout "${IMGCRYPT_VERSION}" && \ + git-checkout-tag-with-hash.sh "${IMGCRYPT_VERSION}" && \ CGO_ENABLED=0 make && DESTDIR=/out make install && \ - echo "- imgcrypt: ${IMGCRYPT_VERSION}" >> /out/share/doc/nerdctl-full/README.md + echo "- imgcrypt: ${IMGCRYPT_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG SLIRP4NETNS_VERSION -RUN fname="slirp4netns-$(cat /target_uname_m)" && \ +RUN SLIRP4NETNS_VERSION=${SLIRP4NETNS_VERSION%%@*}; \ + fname="slirp4netns-$(cat /target_uname_m)" && \ 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 && \ @@ -185,48 +200,58 @@ RUN fname="slirp4netns-$(cat /target_uname_m)" && \ echo "- slirp4netns: ${SLIRP4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md ARG BYPASS4NETNS_VERSION COPY --from=build-bypass4netns /out/${TARGETARCH:-amd64}/* /out/bin/ -RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION}" >> /out/share/doc/nerdctl-full/README.md +RUN echo "- bypass4netns: ${BYPASS4NETNS_VERSION%%@*}" >> /out/share/doc/nerdctl-full/README.md ARG FUSE_OVERLAYFS_VERSION -RUN fname="fuse-overlayfs-$(cat /target_uname_m)" && \ +RUN FUSE_OVERLAYFS_VERSION=${FUSE_OVERLAYFS_VERSION%%@*}; \ + fname="fuse-overlayfs-$(cat /target_uname_m)" && \ 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" && \ +RUN CONTAINERD_FUSE_OVERLAYFS_VERSION=${CONTAINERD_FUSE_OVERLAYFS_VERSION%%@*}; \ + fname="containerd-fuse-overlayfs-${CONTAINERD_FUSE_OVERLAYFS_VERSION##*v}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ 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 TINI_VERSION -RUN fname="tini-static-${TARGETARCH:-amd64}" && \ +RUN TINI_VERSION=${TINI_VERSION%%@*}; \ + fname="tini-static-${TARGETARCH:-amd64}" && \ 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" && \ +# FIXME: this is a mildly-confusing approach. Buildkit will perform some "smart" replacement at build time and output +# confusing debugging information, eg: BUILDG_VERSION will appear as if the original ARG value was used. +RUN BUILDG_VERSION=${BUILDG_VERSION%%@*}; \ + fname="buildg-${BUILDG_VERSION}-${TARGETOS:-linux}-${TARGETARCH:-amd64}.tar.gz" && \ 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" && \ +RUN ROOTLESSKIT_VERSION=${ROOTLESSKIT_VERSION%%@*}; \ + 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 +ARG GOMODJAIL_VERSION +COPY --from=build-gomodjail /out/${TARGETARCH:-amd64}/* /out/bin/ +RUN echo "- gomodjail: ${GOMODJAIL_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/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/{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 "- 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 FROM build-dependencies AS build-full @@ -234,6 +259,10 @@ 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 +# FIXME: `gomodjail pack` depends on QEMU for non-native architecture +# TODO: gomodjail should provide a plain shell script that utilizes `zip(1)` for packing the self-extract archive, without running `gomodjail pack`.. +RUN /out/bin/gomodjail pack --go-mod=/go/src/github.com/containerd/nerdctl/go.mod /out/bin/nerdctl && \ + cp -a nerdctl.gomodjail /out/bin/ COPY README.md /out/share/doc/nerdctl/ COPY docs /out/share/doc/nerdctl/docs RUN (cd /out && find ! -type d | sort | xargs sha256sum > /tmp/SHA256SUMS ) && \ @@ -273,12 +302,14 @@ CMD ["bash", "--login", "-i"] FROM base AS test-integration ARG DEBIAN_FRONTEND=noninteractive # `expect` package contains `unbuffer(1)`, which is used for emulating TTY for testing +# `jq` is required to generate test summaries RUN apt-get update -qq && apt-get install -qq --no-install-recommends \ expect \ + jq \ git \ make # We wouldn't need this if Docker Hub could have "golang:${GO_VERSION}-ubuntu" -COPY --from=build-base-debian /usr/local/go /usr/local/go +COPY --from=build-base /usr/local/go /usr/local/go ARG TARGETARCH ENV PATH=/usr/local/go/bin:$PATH ARG GOTESTSUM_VERSION diff --git a/Dockerfile.d/SHA256SUMS.d/buildg-v0.4.1 b/Dockerfile.d/SHA256SUMS.d/buildg-v0.4.1 deleted file mode 100644 index e01ee1dbdc5..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildg-v0.4.1 +++ /dev/null @@ -1,2 +0,0 @@ -87d047c4742b904e9f0f48427aec5cd157dc96ea97cd89e3ff5b1db171c6eb5e buildg-v0.4.1-linux-amd64.tar.gz -44ab3251cef95f0e79e94f54113be962dacf197ad8d5c5b455aa4a6b8d566111 buildg-v0.4.1-linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 b/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 new file mode 100644 index 00000000000..0e0aa45cbf4 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildg-v0.5.3 @@ -0,0 +1,4 @@ +cf4c40c58ca795eeb6e75e2c6a0e5bb3a6a9c0623d51bc3b85163e5d483eeade buildg-full-v0.5.3-linux-amd64.tar.gz +47c479f2e5150c9c76294fa93a03ad20e5928f4315bf52ca8432bfb6707d4276 buildg-full-v0.5.3-linux-arm64.tar.gz +c289a454ae8673ff99acf56dec9ba97274c20d2015e80f7ac3b8eb8e4f77888f buildg-v0.5.3-linux-amd64.tar.gz +b2e244250ce7ea5c090388f2025a9c546557861d25bba7b0666aa512f01fa6cd buildg-v0.5.3-linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.20.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.20.1 deleted file mode 100644 index d745717f958..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.20.1 +++ /dev/null @@ -1,2 +0,0 @@ -a52768180e2fc1e5ce5ba7c039dcb38ee22ec25e230d34bc4fb3c60159f71be0 buildkit-v0.20.1.linux-amd64.tar.gz -0023f4304cd8e470a6a21f4272bbf10a4d02b51252f17ef22cc272d642c9cf23 buildkit-v0.20.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 new file mode 100644 index 00000000000..853b7c35172 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.21.1 @@ -0,0 +1,2 @@ +e0d83a631a48f13232fcee71cbd913e6b11dbde0a45985fa1b99af27ab97086e buildkit-v0.21.1.linux-amd64.tar.gz +7652a05f2961c386ea6e65c4701daa0e5a899a20c77596cd5f0eca02851dc1f6 buildkit-v0.21.1.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2 deleted file mode 100644 index 109168fb84f..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.6.2 +++ /dev/null @@ -1,2 +0,0 @@ -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/cni-plugins-v1.7.1 b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 new file mode 100644 index 00000000000..c9f57e39739 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/cni-plugins-v1.7.1 @@ -0,0 +1,2 @@ +1a28a0506bfe5bcdc981caf1a49eeab7e72da8321f1119b7be85f22621013098 cni-plugins-linux-amd64-v1.7.1.tgz +119fcb508d1ac2149e49a550752f9cd64d023a1d70e189b59c476e4d2bf7c497 cni-plugins-linux-arm64-v1.7.1.tgz diff --git a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1 deleted file mode 100644 index 6596447644d..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.1 +++ /dev/null @@ -1,6 +0,0 @@ -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/containerd-fuse-overlayfs-v2.1.6 b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 new file mode 100644 index 00000000000..b76b93d4d62 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/containerd-fuse-overlayfs-v2.1.6 @@ -0,0 +1,6 @@ +8a768e4c953251d32b5e5d748d17593f7150834caaba403b483cf83f5856fea3 containerd-fuse-overlayfs-2.1.6-linux-amd64.tar.gz +a3af866a12e913cd1d4dda8e41c08345eca928a15ac1d466fdb2b00b013e14ee containerd-fuse-overlayfs-2.1.6-linux-arm-v7.tar.gz +417ca0c838e43e446f498b384d73f7caaeb00dc4c1c0fe4b0ecfdd36fd355daa containerd-fuse-overlayfs-2.1.6-linux-arm64.tar.gz +5fdebd9fb7b50473318f0410bc3ab46f3388ac8aa586b45c91a314af9ce6569c containerd-fuse-overlayfs-2.1.6-linux-ppc64le.tar.gz +7e1a9d2ba68ff31a8dfb53bf6e71b2879063b13c759922c8cff3013893829bca containerd-fuse-overlayfs-2.1.6-linux-riscv64.tar.gz +3c022651cdaff666e88996d5d9c7e776bf59419a03d7d718a28aa708036419f9 containerd-fuse-overlayfs-2.1.6-linux-s390x.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14 deleted file mode 100644 index 4ef7dca0da1..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.14 +++ /dev/null @@ -1,2 +0,0 @@ -bf2c19b80e68afe1f53bae7a08cc9e7fb2f1b49bfdb9e5b49ab87cbe80b97cd1 fuse-overlayfs-aarch64 -4817a8896a9e6f0433080f88f5b71dec931e8829a89d64c71af94b0630ccb4a9 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 new file mode 100644 index 00000000000..f3eea29017e --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.15 @@ -0,0 +1,6 @@ +a62829baa7a7d39d0a9a784d51ebd528efe226192c0a86ba6667d0fcae9129c3 fuse-overlayfs-aarch64 +7ad67a810100bebf63c41fbb621df3d552531db94d600a94f5f701b1e9f8aa5a fuse-overlayfs-armv7l +9778e1f0da1429469bcc65ea90a7504e63f0a258089b9bb1ae65105330e61808 fuse-overlayfs-ppc64le +f7a2852983b3d0a8f15c31084c215b4965d5b62b9ce1014708283dd2dd909b28 fuse-overlayfs-riscv64 +89a410a67822002c20ff21d8a9e5353ebda00d3a2f79fd99f26fb47533e253a5 fuse-overlayfs-s390x +1cd97f5ca7ac52fa192c94c1e605713cfb27d3dc417c0bef4dcfb9fb20e01e81 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.4 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.4 deleted file mode 100644 index 00b02832926..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.4 +++ /dev/null @@ -1,6 +0,0 @@ -bc07dc45ebc9d19da033ddf4f2f93588d0acef5461d4daabb818f27942db6a22 rootlesskit-aarch64.tar.gz -6d6b9d4749e7e20fea70b63e924323a735fbe7582bdc155cc1332bfd52952b18 rootlesskit-armv7l.tar.gz -db320896ae94cfc08cf1765170c44ea67d3a87d8566e5ce55ca4650fdb7117b9 rootlesskit-ppc64le.tar.gz -d17c9a854277f00bb9285495baf74cbcb7cfd970287dd0e68d7c679d112c05f5 rootlesskit-riscv64.tar.gz -64302b1816aeb9f0b9e80834409c46e0603fb9bedac840f8c6d3b3281fe84b7e rootlesskit-s390x.tar.gz -86639a7d1af2b37413cf98d390b4c38597b88d6ab48749985c0ea30b6fb9eed7 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 new file mode 100644 index 00000000000..96d484fe5c7 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v2.3.5 @@ -0,0 +1,6 @@ +478c14c3195bf989cd9a8e6bd129d227d5d88f1c11418967ffdc84a0072cc7a2 rootlesskit-aarch64.tar.gz +0622e52952a848219b86b902c9bdb96e1ebe575a3015c05e7da02569e83b3a61 rootlesskit-armv7l.tar.gz +b1ec12321c54860230c5d0bbbc6d651a746ac49bce7eeb36fd1ad1e0f0048d58 rootlesskit-ppc64le.tar.gz +8ee59e518cdb5770afab49307b400f585598ed2c06b4ffc81f7c36fbeea422d6 rootlesskit-riscv64.tar.gz +2a3198947cf322357106557c58a8d5f29a664961edf290ea305c94b03521f6c8 rootlesskit-s390x.tar.gz +118208e25becd144ee7317c172fc9decce7b16174d5c1bbf80f1d1d0eacc6b5f rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1 deleted file mode 100644 index 4d0d9ea9444..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.1 +++ /dev/null @@ -1,6 +0,0 @@ -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/slirp4netns-v1.3.2 b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 new file mode 100644 index 00000000000..db7c5ae07df --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/slirp4netns-v1.3.2 @@ -0,0 +1,7 @@ +b4162d27bbbd3683ca8ee57b51a1b270c0054b3a15fcc1830a5d7c10b77ad045 SOURCE_DATE_EPOCH +c55117faa5e18345a3ee1515267f056822ff0c1897999ae5422b0114ee48df85 slirp4netns-aarch64 +f55a6c9e3ec8280e9c3cec083f07dc124e2846ce8139a9281c35013e968d7e95 slirp4netns-armv7l +7b388a9cacbd89821f7f7a6457470fcae8f51aa846162521589feb4634ec7586 slirp4netns-ppc64le +041f9fe507510de1fbb802933a6add093ff19f941185965295c81f2ba4fc9cec slirp4netns-riscv64 +aa39cf14414ae53dbff6b79dfdfa55b5ff8ac5250e2261804863cd365b33a818 slirp4netns-s390x +4d55a3658ae259e3e74bb75cf058eb05d6e39ad6bbe170ca8e94c2462bea0eb1 slirp4netns-x86_64 diff --git a/Dockerfile.d/test-integration-rootless.sh b/Dockerfile.d/test-integration-rootless.sh index 481610eb1bc..f6e243f32b5 100755 --- a/Dockerfile.d/test-integration-rootless.sh +++ b/Dockerfile.d/test-integration-rootless.sh @@ -26,7 +26,7 @@ if [[ "$(id -u)" = "0" ]]; then fi : "${WORKAROUND_ISSUE_622:=}" - if [[ "$WORKAROUND_ISSUE_622" = "1" ]]; then + if [[ "$WORKAROUND_ISSUE_622" != "" ]]; then touch /workaround-issue-622 fi diff --git a/MAINTAINERS b/MAINTAINERS index 8fbc21ebdf6..d245e39c902 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -22,6 +22,7 @@ # GitHub ID, Name, Email address, GPG fingerprint "jsturtevant","James Sturtevant","jstur@microsoft.com","" "manugupt1", "Manu Gupta", "manugupt1@gmail.com","FCA9 504A 4118 EA5C F466 CC30 A5C3 A8F4 E7FE 9E10" +"Shubhranshu153","Shubharanshu Mahapatra","shubhum@amazon.com","" # EMERITUS # See EMERITUS.md diff --git a/Makefile b/Makefile index 331a5022173..65ec10c1c9a 100644 --- a/Makefile +++ b/Makefile @@ -22,11 +22,11 @@ # Configuration ########################## PACKAGE := "github.com/containerd/nerdctl/v2" -ORG_PREFIXES := "github.com/containerd" DOCKER ?= docker GO ?= go GOOS ?= $(shell $(GO) env GOOS) +GOARCH ?= $(shell $(GO) env GOARCH) ifeq ($(GOOS),windows) BIN_EXT := .exe endif @@ -39,9 +39,9 @@ DOCDIR ?= $(DATADIR)/doc BINARY ?= "nerdctl" MAKEFILE_DIR := $(patsubst %/,%,$(dir $(abspath $(lastword $(MAKEFILE_LIST))))) -VERSION ?= $(shell git -C $(MAKEFILE_DIR) describe --match 'v[0-9]*' --dirty='.m' --always --tags) +VERSION ?= $(shell git -C $(MAKEFILE_DIR) describe --match 'v[0-9]*' --dirty='.m' --always --tags 2>/dev/null || echo no_git_information) 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) +REVISION ?= $(shell git -C $(MAKEFILE_DIR) rev-parse HEAD 2>/dev/null || echo no_git_information)$(shell if ! git -C $(MAKEFILE_DIR) diff --no-ext-diff --quiet --exit-code 2>/dev/null; then echo .m; fi) LINT_COMMIT_RANGE ?= main..HEAD GO_BUILD_LDFLAGS ?= -s -w GO_BUILD_FLAGS ?= @@ -80,9 +80,9 @@ endef ########################## all: binaries -lint: lint-go-all lint-imports lint-yaml lint-shell lint-commits lint-mod lint-licenses-all +lint: lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses-all -fix: fix-mod fix-imports fix-go-all +fix: fix-mod fix-go-all # TODO: fix race task and add it test: test-unit # test-unit-race test-unit-bench @@ -103,7 +103,7 @@ help: binaries: $(CURDIR)/_output/$(BINARY)$(BIN_EXT) $(CURDIR)/_output/$(BINARY)$(BIN_EXT): - $(call title, $@) + $(call title, $@: $(GOOS)/$(GOARCH)) $(GO_BUILD) $(GO_BUILD_FLAGS) $(VERBOSE_FLAG) -o $(CURDIR)/_output/$(BINARY)$(BIN_EXT) ./cmd/nerdctl $(call footer, $@) @@ -136,13 +136,8 @@ lint-go-all: @cd $(MAKEFILE_DIR) \ && GOOS=linux make lint-go \ && GOOS=windows make lint-go \ - && GOOS=freebsd make lint-go - $(call footer, $@) - -lint-imports: - $(call title, $@) - @cd $(MAKEFILE_DIR) \ - && goimports-reviser -recursive -list-diff -set-exit-status -output stdout -company-prefixes "$(ORG_PREFIXES)" ./... + && GOOS=freebsd make lint-go \ + && GOOS=darwin make lint-go $(call footer, $@) lint-yaml: @@ -185,8 +180,9 @@ lint-licenses-all: $(call title, $@) @cd $(MAKEFILE_DIR) \ && GOOS=linux make lint-licenses \ + && GOOS=windows make lint-licenses \ && GOOS=freebsd make lint-licenses \ - && GOOS=windows make lint-licenses + && GOOS=darwin make lint-go $(call footer, $@) ########################## @@ -202,14 +198,9 @@ fix-go-all: $(call title, $@) @cd $(MAKEFILE_DIR) \ && GOOS=linux make fix-go \ + && GOOS=windows make fix-go \ && GOOS=freebsd make fix-go \ - && GOOS=windows make fix-go - $(call footer, $@) - -fix-imports: - $(call title, $@) - @cd $(MAKEFILE_DIR) \ - && goimports-reviser -company-prefixes $(ORG_PREFIXES) ./... + && GOOS=darwin make lint-go $(call footer, $@) fix-mod: @@ -223,19 +214,17 @@ fix-mod: ########################## install-dev-tools: $(call title, $@) - # golangci: v1.64.5 - # git-validation: main from 2023/11 - # ltag: v0.2.5 - # go-licenses: v2.0.0-alpha.1 - # goimports-reviser: v3.8.2 + # golangci: v2.0.2 (2024-03-26) + # git-validation: main (2025-02-25) + # ltag: main (2025-03-04) + # go-licenses: v2.0.0-alpha.1 (2024-06-27) @cd $(MAKEFILE_DIR) \ - && go install github.com/golangci/golangci-lint/cmd/golangci-lint@0a603e49e5e9870f5f9f2035bcbe42cd9620a9d5 \ - && go install github.com/vbatts/git-validation@679e5cad8c50f1605ab3d8a0a947aaf72fb24c07 \ - && go install github.com/kunalkushwaha/ltag@b0cfa33e4cc9383095dc584d3990b62c95096de0 \ + && go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@2b224c2cf4c9f261c22a16af7f8ca6408467f338 \ + && go install github.com/vbatts/git-validation@7b60e35b055dd2eab5844202ffffad51d9c93922 \ + && go install github.com/containerd/ltag@66e6a514664ee2d11a470735519fa22b1a9eaabd \ && go install github.com/google/go-licenses/v2@d01822334fba5896920a060f762ea7ecdbd086e8 \ - && go install github.com/incu6us/goimports-reviser/v3@f034195cc8a7ffc7cc70d60aa3a25500874eaf04 \ && go install gotest.tools/gotestsum@ac6dad9c7d87b969004f7749d1942938526c9716 - @echo "Remember to add GOROOT/bin to your path" + @echo "Remember to add \$$HOME/go/bin to your path" $(call footer, $@) ########################## @@ -279,6 +268,9 @@ artifacts: clean 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=loong64 make -C $(CURDIR) -f $(MAKEFILE_DIR)/Makefile binaries + tar $(TAR_OWNER0_FLAGS) $(TAR_FLATTEN_FLAGS) -czvf $(CURDIR)/_output/nerdctl-$(VERSION_TRIMMED)-linux-loong64.tar.gz $(CURDIR)/_output/nerdctl $(MAKEFILE_DIR)/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/* @@ -312,8 +304,8 @@ artifacts: clean binaries \ install \ clean \ - lint-go lint-go-all lint-imports lint-yaml lint-shell lint-commits lint-mod lint-licenses lint-licenses-all \ - fix-go fix-go-all fix-imports fix-mod \ + lint-go lint-go-all lint-yaml lint-shell lint-commits lint-mod lint-licenses lint-licenses-all \ + fix-go fix-go-all fix-mod \ install-dev-tools \ test-unit test-unit-race test-unit-bench \ artifacts diff --git a/Vagrantfile.freebsd b/Vagrantfile.freebsd index f3a2e4a4d4a..a1928268038 100644 --- a/Vagrantfile.freebsd +++ b/Vagrantfile.freebsd @@ -36,6 +36,10 @@ Vagrant.configure("2") do |config| sh.inline = <<~SHELL #!/usr/bin/env bash set -eux -o pipefail + freebsd-version -kru + # switching to "release_2" ensures compatibility with the current Vagrant box + # https://github.com/moby/buildkit/pull/5893 + sed -i '' 's/latest/release_2/' /usr/local/etc/pkg/repos/FreeBSD.conf # `pkg install go` still installs Go 1.20 (March 2024) pkg install -y go122 containerd runj ln -s go122 /usr/local/bin/go @@ -59,7 +63,7 @@ Vagrant.configure("2") do |config| 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." + CONTAINERD_ADDRESS=/run/containerd/containerd.sock /root/go/bin/nerdctl run --rm --quiet --net=none dougrabson/freebsd-minimal:13 echo "Nerdctl is up and running." SHELL end diff --git a/cmd/nerdctl/builder/builder.go b/cmd/nerdctl/builder/builder.go index 46ae2cfc66d..6154090affd 100644 --- a/cmd/nerdctl/builder/builder.go +++ b/cmd/nerdctl/builder/builder.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "strings" + "time" "github.com/docker/go-units" "github.com/spf13/cobra" @@ -58,8 +59,7 @@ func pruneCommand() *cobra.Command { SilenceErrors: true, } - helpers.AddStringFlag(cmd, "buildkit-host", nil, "", "BUILDKIT_HOST", "BuildKit address") - + cmd.Flags().String("buildkit-host", "", "BuildKit address") cmd.Flags().BoolP("all", "a", false, "Remove all unused build cache, not just dangling ones") cmd.Flags().BoolP("force", "f", false, "Do not prompt for confirmation") return cmd @@ -147,6 +147,7 @@ func debugCommand() *cobra.Command { cmd.Flags().String("image", "", "Image to use for debugging stage") cmd.Flags().StringArray("ssh", nil, "Allow forwarding SSH agent to the build. Format: default|[=|[,]]") cmd.Flags().StringArray("secret", nil, "Expose secret value to the build. Format: id=secretname,src=filepath") + helpers.AddDurationFlag(cmd, "buildg-startup-timeout", nil, 1*time.Minute, "", "Timeout for starting up buildg") return cmd } @@ -168,6 +169,12 @@ func debugAction(cmd *cobra.Command, args []string) error { buildgArgs = append([]string{"--debug"}, buildgArgs...) } + startupTimeout, err := cmd.Flags().GetDuration("buildg-startup-timeout") + if err != nil { + return err + } + buildgArgs = append(buildgArgs, "--startup-timeout="+startupTimeout.String()) + if file, err := cmd.Flags().GetString("file"); err != nil { return err } else if file != "" { diff --git a/cmd/nerdctl/builder/builder_build.go b/cmd/nerdctl/builder/builder_build.go index 52a9f108620..8b9691fb8a3 100644 --- a/cmd/nerdctl/builder/builder_build.go +++ b/cmd/nerdctl/builder/builder_build.go @@ -25,6 +25,8 @@ import ( "github.com/spf13/cobra" + "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/api/types" @@ -44,7 +46,7 @@ If Dockerfile is not present and -f is not specified, it will look for Container SilenceUsage: true, SilenceErrors: true, } - helpers.AddStringFlag(cmd, "buildkit-host", nil, "", "BUILDKIT_HOST", "BuildKit address") + cmd.Flags().String("buildkit-host", "", "BuildKit address") cmd.Flags().StringArray("add-host", nil, "Add a custom host-to-IP mapping (format: \"host:ip\")") cmd.Flags().StringArrayP("tag", "t", nil, "Name and optionally a tag in the 'name:tag' format") cmd.Flags().StringP("file", "f", "", "Name of the Dockerfile") @@ -209,6 +211,13 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu return types.BuilderBuildOptions{}, err } + usernsRemap, err := cmd.Flags().GetString("userns-remap") + if err != nil { + return types.BuilderBuildOptions{}, err + } else if usernsRemap != "" { + log.L.Warn("userns remap is not supported with nerdctl build. dropping the config.") + } + return types.BuilderBuildOptions{ GOptions: globalOptions, BuildKitHost: buildKitHost, @@ -242,7 +251,7 @@ func processBuildCommandFlag(cmd *cobra.Command, args []string) (types.BuilderBu } func GetBuildkitHost(cmd *cobra.Command, namespace string) (string, error) { - if cmd.Flags().Changed("buildkit-host") || os.Getenv("BUILDKIT_HOST") != "" { + if cmd.Flags().Changed("buildkit-host") { // If address is explicitly specified, use it. buildkitHost, err := cmd.Flags().GetString("buildkit-host") if err != nil { @@ -253,6 +262,14 @@ func GetBuildkitHost(cmd *cobra.Command, namespace string) (string, error) { } return buildkitHost, nil } + + if buildkitHost := os.Getenv("BUILDKIT_HOST"); buildkitHost != "" { + if err := buildkitutil.PingBKDaemon(buildkitHost); err != nil { + return "", err + } + return buildkitHost, nil + + } return buildkitutil.GetBuildkitHost(namespace) } diff --git a/cmd/nerdctl/builder/builder_build_oci_layout_test.go b/cmd/nerdctl/builder/builder_build_oci_layout_test.go index 93cc4152284..758675e85a1 100644 --- a/cmd/nerdctl/builder/builder_build_oci_layout_test.go +++ b/cmd/nerdctl/builder/builder_build_oci_layout_test.go @@ -18,13 +18,13 @@ package builder import ( "fmt" - "os" "path/filepath" "strings" "testing" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -44,17 +44,17 @@ func TestBuildContextWithOCILayout(t *testing.T) { ), 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("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")) + 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") + name := data.Identifier("container") helpers.Ensure("buildx", "create", "--name", name, "--driver=docker-container") dockerBuilderArgs = []string{"buildx", "--builder", name} } @@ -63,25 +63,21 @@ func TestBuildContextWithOCILayout(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") + dest := data.Temp().Dir("parent") + tarPath := data.Temp().Path("parent.tar") - 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{}) + helpers.Ensure("build", data.Temp().Path(), "--tag", data.Identifier("parent")) + helpers.Ensure("image", "save", "--output", tarPath, data.Identifier("parent")) + helpers.Custom("tar", "Cxf", dest, tarPath).Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + }) }, 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) + data.Temp().Save(dockerfile, "Dockerfile") var cmd test.TestableCommand if nerdtest.IsDocker() { @@ -89,7 +85,13 @@ CMD ["echo", "test-nerdctl-build-context-oci-layout"]` } else { cmd = helpers.Command() } - cmd.WithArgs("build", buildCtx, fmt.Sprintf("--build-context=parent=oci-layout://%s", filepath.Join(buildCtx, "parent")), "--tag", data.Identifier("-child")) + cmd.WithArgs( + "build", + data.Temp().Path(), + fmt.Sprintf("--build-context=parent=oci-layout://%s", filepath.Join(data.Temp().Path(), "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") @@ -99,7 +101,14 @@ CMD ["echo", "test-nerdctl-build-context-oci-layout"]` 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) + assert.Assert( + t, + strings.Contains( + helpers.Capture("run", "--rm", data.Identifier("child")), + "test-nerdctl-build-context-oci-layout", + ), + info, + ) }, } }, diff --git a/cmd/nerdctl/builder/builder_build_test.go b/cmd/nerdctl/builder/builder_build_test.go index be8bc051a67..839fd0d6e01 100644 --- a/cmd/nerdctl/builder/builder_build_test.go +++ b/cmd/nerdctl/builder/builder_build_test.go @@ -19,7 +19,6 @@ package builder import ( "errors" "fmt" - "os" "path/filepath" "runtime" "strings" @@ -31,6 +30,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -39,23 +39,20 @@ import ( func TestBuildBasics(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) + 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()) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, 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")) + helpers.Ensure("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) @@ -63,12 +60,12 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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()) + helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) @@ -76,12 +73,18 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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")) + helpers.Ensure( + "build", + data.Labels().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()) @@ -89,20 +92,27 @@ CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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")) + helpers.Ensure( + "build", + data.Labels().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("ignored")) helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(-1, nil, nil), + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, }, } @@ -122,32 +132,39 @@ func TestCanBuildOnOtherPlatform(t *testing.T) { can, err := platformutil.CanExecProbably("linux/" + candidateArch) assert.NilError(helpers.T(), err) - data.Set("OS", "linux") - data.Set("Architecture", candidateArch) + data.Labels().Set("OS", "linux") + data.Labels().Set("Architecture", candidateArch) return can, "Current environment does not support emulation" }, } + dockerfile := fmt.Sprintf(`FROM %s +RUN echo hello > /hello +CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( 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()) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, 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()) + return helpers.Command( + "build", + data.Labels().Get("buildCtx"), + "--platform", + fmt.Sprintf("%s/%s", data.Labels().Get("OS"), data.Labels().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), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) @@ -168,21 +185,19 @@ func TestBuildBaseImage(t *testing.T) { 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()) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("first"), data.Temp().Path()) 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()) + data.Temp().Save(dockerfileSecond, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("second"), data.Temp().Path()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("second")) }, - Expected: test.Expects(0, nil, expect.Equals("hello2\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello2\n")), } testCase.Run(t) @@ -209,14 +224,13 @@ func TestBuildFromContainerd(t *testing.T) { 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()) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", "-t", data.Identifier("second"), data.Temp().Path()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier("second")) }, - Expected: test.Expects(0, nil, expect.Equals("hello2\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello2\n")), } testCase.Run(t) @@ -225,16 +239,17 @@ CMD ["cat", "/hello2"]`, data.Identifier("first")) func TestBuildFromStdin(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-stdin"]`, testutil.CommonImage) + 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)) + cmd.Feed(strings.NewReader(dockerfile)) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -250,21 +265,15 @@ CMD ["echo", "nerdctl-build-test-stdin"]`, testutil.CommonImage) func TestBuildWithDockerfile(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-dockerfile"] + `, testutil.CommonImage) + 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) + data.Temp().Save(dockerfile, "test", "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path("test")) }, SubTests: []*test.Case{ { @@ -274,10 +283,10 @@ CMD ["echo", "nerdctl-build-test-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")) + cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, { Description: "Dockerfile .", @@ -286,16 +295,16 @@ CMD ["echo", "nerdctl-build-test-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")) + cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, 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")) + cmd.WithCwd(data.Labels().Get("buildCtx")) return cmd }, Expected: test.Expects(1, nil, nil), @@ -312,53 +321,41 @@ func TestBuildLocal(t *testing.T) { const testFileName = "nerdctl-build-test" const testContent = "nerdctl" + dockerfile := fmt.Sprintf(`FROM scratch +COPY %s /`, testFileName) + 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()) + data.Temp().Save(dockerfile, "Dockerfile") + data.Temp().Save(testContent, testFileName) + data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { - Description: "destination 1", + // GOTCHA: avoid comma and = in the test name, or buildctl will misparse the destination direction + Description: "-o type local destination DIR: verify the file copied from context is in the output directory", 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")) + return helpers.Command("build", "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path()), data.Labels().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) + Output: func(stdout, info string, t *testing.T) { + // Expecting testFileName to exist inside the output target directory + assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } }, }, { - Description: "destination 2", + Description: "-o DIR: verify the file copied from context is in the output directory", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("build", "-o", data.TempDir(), data.Get("buildCtx")) + return helpers.Command("build", "-o", data.Temp().Path(), data.Labels().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) + Output: func(stdout, info string, t *testing.T) { + assert.Equal(t, data.Temp().Load(testFileName), testContent, "file content is identical") }, } }, @@ -372,27 +369,26 @@ COPY %s /`, testFileName) func TestBuildWithBuildArg(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +ARG TEST_STRING=1 +ENV TEST_STRING=$TEST_STRING +CMD echo $TEST_STRING + `, testutil.CommonImage) + 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) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { Description: "No args", Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier()) + helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) @@ -400,12 +396,12 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("1\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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()) + helpers.Ensure("build", data.Labels().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()) @@ -413,12 +409,12 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("2\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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()) + helpers.Ensure("build", data.Labels().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()) @@ -426,12 +422,12 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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()) + helpers.Ensure("build", data.Labels().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()) @@ -439,7 +435,7 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("1\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1\n")), }, { Description: "EnvValueOverridesDefault", @@ -447,7 +443,7 @@ CMD echo $TEST_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()) + helpers.Ensure("build", data.Labels().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()) @@ -455,7 +451,7 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("3\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("3\n")), }, { Description: "EmptyEnvValueOverridesDefault", @@ -463,7 +459,7 @@ CMD echo $TEST_STRING "TEST_STRING": "", }, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("build", data.Get("buildCtx"), "--build-arg", "TEST_STRING", "-t", data.Identifier()) + helpers.Ensure("build", data.Labels().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()) @@ -471,7 +467,7 @@ CMD echo $TEST_STRING Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("\n")), }, }, } @@ -482,27 +478,24 @@ CMD echo $TEST_STRING func TestBuildWithIIDFile(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + 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()) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path(), "--iidfile", data.Temp().Path("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)) + return helpers.Command("run", "--rm", data.Temp().Load("id.txt")) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), } testCase.Run(t) @@ -511,25 +504,24 @@ CMD ["echo", "nerdctl-build-test-string"] func TestBuildWithLabels(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +LABEL name=nerdctl-build-test-label + `, testutil.CommonImage) + 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()) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path(), "--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, expect.Equals("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("{\"label\":\"test\",\"name\":\"nerdctl-build-test-label\"}\n")), } testCase.Run(t) @@ -538,49 +530,54 @@ LABEL name=nerdctl-build-test-label func TestBuildMultipleTags(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + 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")) + helpers.Anyhow("rmi", "-f", data.Labels().Get("i1")) + helpers.Anyhow("rmi", "-f", data.Labels().Get("i2")) + helpers.Anyhow("rmi", "-f", data.Labels().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")) + data.Labels().Set("i1", data.Identifier("image")) + data.Labels().Set("i2", data.Identifier("image2")) + data.Labels().Set("i3", data.Identifier("image3")+":hello") + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure( + "build", + data.Temp().Path(), + "-t", data.Labels().Get("i1"), + "-t", data.Labels().Get("i2"), + "-t", data.Labels().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")) + return helpers.Command("run", "--rm", data.Labels().Get("i1")) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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")) + return helpers.Command("run", "--rm", data.Labels().Get("i2")) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.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")) + return helpers.Command("run", "--rm", data.Labels().Get("i3")) }, - Expected: test.Expects(0, nil, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), }, }, } @@ -591,6 +588,10 @@ CMD ["echo", "nerdctl-build-test-string"] func TestBuildWithContainerfile(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( nerdtest.Build, @@ -600,18 +601,13 @@ func TestBuildWithContainerfile(t *testing.T) { 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()) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path(), "-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, expect.Equals("nerdctl-build-test-string\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("nerdctl-build-test-string\n")), } testCase.Run(t) @@ -629,20 +625,19 @@ func TestBuildWithDockerFileAndContainerfile(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") + 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()) + data.Temp().Save(dockerfile, "Containerfile") + + helpers.Ensure("build", data.Temp().Path(), "-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, expect.Equals("dockerfile\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("dockerfile\n")), } testCase.Run(t) @@ -651,6 +646,10 @@ CMD ["echo", "containerfile"] func TestBuildNoTag(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + // 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, @@ -659,16 +658,13 @@ func TestBuildNoTag(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") + + // XXX FIXME + helpers.Capture("build", data.Temp().Path()) }, Command: test.Command("images"), - Expected: test.Expects(0, nil, expect.Contains("")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("")), } testCase.Run(t) @@ -677,23 +673,27 @@ CMD ["echo", "nerdctl-build-test-string"] func TestBuildContextDockerImageAlias(t *testing.T) { nerdtest.Setup() + dockerfile := `FROM myorg/myapp +CMD ["echo", "nerdctl-build-myorg/myapp"]` + 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) + data.Temp().Save(dockerfile, "Dockerfile") }, 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)) + return helpers.Command( + "build", + "-t", + data.Identifier(), + data.Temp().Path(), + fmt.Sprintf("--build-context=myorg/myapp=docker-image://%s", testutil.CommonImage), + ) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) @@ -704,6 +704,9 @@ func TestBuildContextWithCopyFromDir(t *testing.T) { content := "hello_from_dir_2" filename := "hello.txt" + dockerfile := fmt.Sprintf(`FROM %s +COPY --from=dir2 /%s /hello_from_dir2.txt +RUN ["cat", "/hello_from_dir2.txt"]`, testutil.CommonImage, filename) testCase := &test.Case{ Require: require.All( @@ -714,23 +717,19 @@ func TestBuildContextWithCopyFromDir(t *testing.T) { 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) + data.Temp().Save(dockerfile, "context", "Dockerfile") + data.Temp().Save(content, "other-directory", filename) }, 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"))) + return helpers.Command( + "build", + "-t", + data.Identifier(), + data.Temp().Path("context"), + fmt.Sprintf("--build-context=dir2=%s", data.Temp().Path("other-directory")), + ) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) @@ -741,24 +740,20 @@ RUN ["cat", "/hello_from_dir2.txt"]`, testutil.CommonImage, filename) func TestBuildSourceDateEpoch(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +ARG SOURCE_DATE_EPOCH +RUN echo $SOURCE_DATE_EPOCH >/source-date-epoch +CMD ["cat", "/source-date-epoch"] + `, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( nerdtest.Build, require.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) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, SubTests: []*test.Case{ { @@ -767,7 +762,7 @@ CMD ["cat", "/source-date-epoch"] "SOURCE_DATE_EPOCH": "1111111111", }, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("build", data.Get("buildCtx"), "-t", data.Identifier()) + helpers.Ensure("build", data.Labels().Get("buildCtx"), "-t", data.Identifier()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) @@ -775,7 +770,7 @@ CMD ["cat", "/source-date-epoch"] Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("1111111111\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("1111111111\n")), }, { Description: "2222222222", @@ -783,7 +778,7 @@ CMD ["cat", "/source-date-epoch"] "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()) + helpers.Ensure("build", data.Labels().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()) @@ -791,7 +786,7 @@ CMD ["cat", "/source-date-epoch"] Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.Equals("2222222222\n")), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("2222222222\n")), }, }, } @@ -802,29 +797,25 @@ CMD ["cat", "/source-date-epoch"] func TestBuildNetwork(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +RUN apk add --no-cache curl +RUN curl -I http://google.com + `, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( nerdtest.Build, require.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) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, 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") + return helpers.Command("build", data.Labels().Get("buildCtx"), "-t", data.Identifier(), "--no-cache", "--network", "none") }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", data.Identifier()) @@ -834,22 +825,22 @@ RUN curl -I http://google.com { 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", "") + return helpers.Command("build", data.Labels().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), + Expected: test.Expects(expect.ExitCodeSuccess, 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") + return helpers.Command("build", data.Labels().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), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), }, }, } @@ -863,6 +854,8 @@ func TestBuildAttestation(t *testing.T) { const testSBOMFileName = "sbom.spdx.json" const testProvenanceFileName = "provenance.json" + dockerfile := fmt.Sprintf(`FROM %s`, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( nerdtest.Build, @@ -872,38 +865,34 @@ func TestBuildAttestation(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) }, 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")) + cmd.WithArgs( + "--sbom=true", + "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-bom")), + data.Labels().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) + Output: func(stdout, info string, t *testing.T) { + data.Temp().Exists("dir-for-bom", testSBOMFileName) }, } }, @@ -911,21 +900,21 @@ func TestBuildAttestation(t *testing.T) { { 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")) + cmd.WithArgs( + "--provenance=mode=min", + "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-prov")), + data.Labels().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) + Output: func(stdout, info string, t *testing.T) { + data.Temp().Exists("dir-for-prov", testProvenanceFileName) }, } }, @@ -933,24 +922,23 @@ func TestBuildAttestation(t *testing.T) { { 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")) + cmd.WithArgs( + "--attest=type=provenance,mode=min", + "--attest=type=sbom", + "-o", fmt.Sprintf("type=local,dest=%s", data.Temp().Path("dir-for-attest")), + data.Labels().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) + Output: func(stdout, info string, t *testing.T) { + data.Temp().Exists("dir-for-attest", testSBOMFileName) + data.Temp().Exists("dir-for-attest", testProvenanceFileName) }, } }, @@ -964,6 +952,11 @@ func TestBuildAttestation(t *testing.T) { func TestBuildAddHost(t *testing.T) { nerdtest.Setup() + dockerfile := fmt.Sprintf(`FROM %s +RUN ping -c 5 alpha +RUN ping -c 5 beta +`, testutil.CommonImage) + testCase := &test.Case{ Require: require.All( nerdtest.Build, @@ -972,20 +965,102 @@ func TestBuildAddHost(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") }, 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") + return helpers.Command( + "build", data.Temp().Path(), + "-t", data.Identifier(), + "--add-host", "alpha:127.0.0.1", + "--add-host", "beta:127.0.0.1", + ) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } + +func TestBuildWithBuildkitConfig(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.All( + nerdtest.Build, + require.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"]`, testutil.CommonImage) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("buildCtx", data.Temp().Path()) + + }, + SubTests: []*test.Case{ + { + Description: "build with buildkit-host", + Setup: func(data test.Data, helpers test.Helpers) { + // Get BuildkitAddr + buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(helpers.T(), err) + buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") + + // Symlink the buildkit Socket for testing + symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit.sock") + + // Do a negative test to check the setup + helpers.Fail("build", "-t", data.Identifier(), "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr), data.Labels().Get("buildCtx")) + + // Test build with the symlinked socket + cmd := helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + helpers.Ensure("build", "-t", data.Identifier(), "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr), data.Labels().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, expect.Equals("nerdctl-build-test-string\n")), + }, + { + Description: "build with env specified", + Setup: func(data test.Data, helpers test.Helpers) { + // Get BuildkitAddr + buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(helpers.T(), err) + buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") + + // Symlink the buildkit Socket for testing + symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit-env.sock") + + // Do a negative test to ensure setting up the env variable is effective + cmd := helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) + cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) + cmd.Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + // Symlink the buildkit socket for testing + cmd = helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + + cmd = helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) + cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) + cmd.Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, + 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, expect.Equals("nerdctl-build-test-string\n")), + }, + }, + } + testCase.Run(t) +} diff --git a/cmd/nerdctl/builder/builder_builder_test.go b/cmd/nerdctl/builder/builder_builder_test.go index 57c1a864ee3..da912cc0af9 100644 --- a/cmd/nerdctl/builder/builder_builder_test.go +++ b/cmd/nerdctl/builder/builder_builder_test.go @@ -17,18 +17,19 @@ package builder import ( - "bytes" "errors" "fmt" - "os" "path/filepath" + "strings" "testing" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -49,10 +50,8 @@ func TestBuilder(t *testing.T) { 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) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path()) }, Command: test.Command("builder", "prune", "--force"), Expected: test.Expects(0, nil, nil), @@ -63,14 +62,74 @@ CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) 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) + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path()) }, Command: test.Command("builder", "prune", "--force", "--all"), Expected: test.Expects(0, nil, nil), }, + { + Description: "builder with buildkit-host", + NoParallel: true, + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + // Get BuildkitAddr + buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(helpers.T(), err) + buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") + + // Symlink the buildkit Socket for testing + symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit.sock") + data.Labels().Set("symlinkedBuildkitAddr", symlinkedBuildkitAddr) + + // Do a negative test to check the setup + helpers.Fail("builder", "prune", "--force", "--buildkit-host", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) + + // Test build with the symlinked socket + cmd := helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("builder", "prune", "--force", "--buildkit-host", fmt.Sprintf("unix://%s", data.Labels().Get("symlinkedBuildkitAddr"))) + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "builder with env", + NoParallel: true, + Require: require.Not(nerdtest.Docker), + Setup: func(data test.Data, helpers test.Helpers) { + // Get BuildkitAddr + buildkitAddr, err := buildkitutil.GetBuildkitHost(testutil.Namespace) + assert.NilError(helpers.T(), err) + buildkitAddr = strings.TrimPrefix(buildkitAddr, "unix://") + + // Symlink the buildkit Socket for testing + symlinkedBuildkitAddr := filepath.Join(data.Temp().Path(), "buildkit-env.sock") + data.Labels().Set("symlinkedBuildkitAddr", symlinkedBuildkitAddr) + + // Do a negative test to ensure setting up the env variable is effective + cmd := helpers.Command("builder", "prune", "--force") + cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) + cmd.Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + // Symlink the buildkit socket for testing + cmd = helpers.Custom("ln", "-s", buildkitAddr, symlinkedBuildkitAddr) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + symlinkedBuildkitAddr := data.Labels().Get("symlinkedBuildkitAddr") + cmd := helpers.Command("builder", "prune", "--force") + cmd.Setenv("BUILDKIT_HOST", fmt.Sprintf("unix://%s", symlinkedBuildkitAddr)) + return cmd + }, + Expected: test.Expects(0, nil, nil), + }, { Description: "Debug", // `nerdctl builder debug` is currently incompatible with `docker buildx debug`. @@ -79,11 +138,9 @@ CMD ["echo", "nerdctl-test-builder-prune"]`, testutil.CommonImage) 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"))) + data.Temp().Save(dockerfile, "Dockerfile") + cmd := helpers.Command("builder", "debug", data.Temp().Path()) + cmd.Feed(strings.NewReader("c\n")) return cmd }, Expected: test.Expects(0, nil, nil), @@ -103,13 +160,10 @@ CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) 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) + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("oldImageSha", oldImageSha) + data.Labels().Set("newImageSha", newImageSha) + data.Labels().Set("base", data.Temp().Dir()) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", testutil.AlpineImage) @@ -119,11 +173,11 @@ CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) Description: "pull false", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("build", data.Get("buildCtx"), "--pull=false") + return helpers.Command("build", data.Labels().Get("base"), "--pull=false") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Errors: []error{errors.New(data.Get("oldImageSha"))}, + Errors: []error{errors.New(data.Labels().Get("oldImageSha"))}, } }, }, @@ -131,11 +185,11 @@ CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) Description: "pull true", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("build", data.Get("buildCtx"), "--pull=true") + return helpers.Command("build", data.Labels().Get("base"), "--pull=true") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Errors: []error{errors.New(data.Get("newImageSha"))}, + Errors: []error{errors.New(data.Labels().Get("newImageSha"))}, } }, }, @@ -143,11 +197,11 @@ CMD ["echo", "nerdctl-builder-debug-test-string"]`, testutil.CommonImage) Description: "no pull", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("build", data.Get("buildCtx")) + return helpers.Command("build", data.Labels().Get("base")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Errors: []error{errors.New(data.Get("newImageSha"))}, + Errors: []error{errors.New(data.Labels().Get("newImageSha"))}, } }, }, diff --git a/cmd/nerdctl/completion/completion.go b/cmd/nerdctl/completion/completion.go index 7718c1bb063..e12e2375a40 100644 --- a/cmd/nerdctl/completion/completion.go +++ b/cmd/nerdctl/completion/completion.go @@ -164,6 +164,7 @@ func Platforms(cmd *cobra.Command, args []string, toComplete string) ([]string, "riscv64", "ppc64le", "s390x", + "loong64", "386", "arm", // alias of "linux/arm/v7" "linux/arm/v6", // "arm/v6" is invalid (interpreted as OS="arm", Arch="v7") diff --git a/cmd/nerdctl/completion/completion_test.go b/cmd/nerdctl/completion/completion_test.go index 23bdf833c52..c75e37d490c 100644 --- a/cmd/nerdctl/completion/completion_test.go +++ b/cmd/nerdctl/completion/completion_test.go @@ -17,6 +17,7 @@ package completion import ( + "os/exec" "testing" "github.com/containerd/nerdctl/mod/tigron/expect" @@ -34,6 +35,11 @@ func TestMain(m *testing.M) { func TestCompletion(t *testing.T) { nerdtest.Setup() + // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need + // to retrieve the binary name. + // Note that we know this works already, so no need to assert err. + bin, _ := exec.LookPath(testutil.GetTarget()) + testCase := &test.Case{ Require: require.Not(nerdtest.Docker), Setup: func(data test.Data, helpers test.Helpers) { @@ -41,7 +47,7 @@ func TestCompletion(t *testing.T) { helpers.Ensure("pull", "--quiet", testutil.CommonImage) helpers.Ensure("network", "create", identifier) helpers.Ensure("volume", "create", identifier) - data.Set("identifier", identifier) + data.Labels().Set("identifier", identifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() @@ -91,10 +97,7 @@ func TestCompletion(t *testing.T) { Command: test.Command("__complete", "run", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.All( - expect.Contains("host\n"), - expect.Contains(data.Get("identifier")+"\n"), - ), + Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, @@ -103,10 +106,7 @@ func TestCompletion(t *testing.T) { Command: test.Command("__complete", "run", "-it", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.All( - expect.Contains("host\n"), - expect.Contains(data.Get("identifier")+"\n"), - ), + Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, @@ -115,10 +115,7 @@ func TestCompletion(t *testing.T) { Command: test.Command("__complete", "run", "-it", "--rm", "--net", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.All( - expect.Contains("host\n"), - expect.Contains(data.Get("identifier")+"\n"), - ), + Output: expect.Contains("host\n", data.Labels().Get("identifier")+"\n"), } }, }, @@ -134,7 +131,7 @@ func TestCompletion(t *testing.T) { return &test.Expected{ Output: expect.All( expect.DoesNotContain("host\n"), - expect.Contains(data.Get("identifier")+"\n"), + expect.Contains(data.Labels().Get("identifier")+"\n"), ), } }, @@ -153,7 +150,7 @@ func TestCompletion(t *testing.T) { Command: test.Command("__complete", "volume", "inspect", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("identifier") + "\n"), + Output: expect.Contains(data.Labels().Get("identifier") + "\n"), } }, }, @@ -162,22 +159,22 @@ func TestCompletion(t *testing.T) { Command: test.Command("__complete", "volume", "rm", ""), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("identifier") + "\n"), + Output: expect.Contains(data.Labels().Get("identifier") + "\n"), } }, }, { - Description: "no namespace --cgroup-manager", + Description: "--cgroup-manager", Require: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("nerdctl", "__complete", "--cgroup-manager", "") + return helpers.Command("__complete", "--cgroup-manager", "") }, Expected: test.Expects(0, nil, expect.Contains("cgroupfs\n")), }, { - Description: "no namespace empty", + Description: "empty", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("nerdctl", "__complete", "") + return helpers.Command("__complete", "") }, Expected: test.Expects(0, nil, expect.Contains("run\t")), }, @@ -185,7 +182,7 @@ func TestCompletion(t *testing.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)), "") + return helpers.Custom(bin, "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "") }, Expected: test.Expects(0, nil, expect.Contains("run\t")), }, @@ -208,7 +205,7 @@ func TestCompletion(t *testing.T) { 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", "") + return helpers.Custom(bin, "__complete", "--namespace", string(helpers.Read(nerdtest.Namespace)), "run", "-i", "") }, Expected: test.Expects(0, nil, expect.Contains(testutil.CommonImage+"\n")), }, diff --git a/cmd/nerdctl/completion/completion_freebsd.go b/cmd/nerdctl/completion/completion_unix_nolinux.go similarity index 96% rename from cmd/nerdctl/completion/completion_freebsd.go rename to cmd/nerdctl/completion/completion_unix_nolinux.go index 465671cfc24..5a80c80573c 100644 --- a/cmd/nerdctl/completion/completion_freebsd.go +++ b/cmd/nerdctl/completion/completion_unix_nolinux.go @@ -1,3 +1,5 @@ +//go:build unix && !linux + /* Copyright The containerd Authors. diff --git a/cmd/nerdctl/compose/compose_build_linux_test.go b/cmd/nerdctl/compose/compose_build_linux_test.go index 80cc04d4c35..d0092f0a9df 100644 --- a/cmd/nerdctl/compose/compose_build_linux_test.go +++ b/cmd/nerdctl/compose/compose_build_linux_test.go @@ -17,23 +17,35 @@ package compose import ( + "errors" "fmt" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeBuild(t *testing.T) { - const imageSvc0 = "composebuild_svc0" - const imageSvc1 = "composebuild_svc1" + dockerfile := "FROM " + testutil.AlpineImage + + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.Build + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Make sure we shard the image name to something unique to the test to avoid conflicts with other tests + imageSvc0 := data.Identifier("svc0") + imageSvc1 := data.Identifier("svc1") - dockerComposeYAML := fmt.Sprintf(` + // We are not going to run them, so, ports conflicts should not matter here + dockerComposeYAML := fmt.Sprintf(` services: svc0: build: . image: %s - ports: - - 8080:80 depends_on: - svc1 svc1: @@ -43,31 +55,78 @@ services: - 8081:80 `, imageSvc0, imageSvc1) - dockerfile := fmt.Sprintf(`FROM %s`, testutil.AlpineImage) - - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - base := testutil.NewBase(t) - - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - comp.WriteFile("Dockerfile", dockerfile) - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - defer base.Cmd("rmi", imageSvc0).Run() - defer base.Cmd("rmi", imageSvc1).Run() - - // 1. build only 1 service without triggering the dependency service build - base.ComposeCmd("-f", comp.YAMLFullPath(), "build", "svc0").AssertOK() - base.Cmd("images").AssertOutContains(imageSvc0) - base.Cmd("images").AssertOutNotContains(imageSvc1) - // 2. build multiple services - base.ComposeCmd("-f", comp.YAMLFullPath(), "build", "svc0", "svc1").AssertOK() - base.Cmd("images").AssertOutContains(imageSvc0) - base.Cmd("images").AssertOutContains(imageSvc1) - // 3. build all if no args are given - base.ComposeCmd("-f", comp.YAMLFullPath(), "build").AssertOK() - // 4. fail if some services args not exist in compose.yml - base.ComposeCmd("-f", comp.YAMLFullPath(), "build", "svc0", "svc100").AssertFail() + data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Temp().Save(dockerfile, "Dockerfile") + + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + data.Labels().Set("imageSvc0", imageSvc0) + data.Labels().Set("imageSvc1", imageSvc1) + } + + testCase.SubTests = []*test.Case{ + { + Description: "build svc0", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0") + }, + + Command: test.Command("images"), + + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + expect.Contains(data.Labels().Get("imageSvc0")), + expect.DoesNotContain(data.Labels().Get("imageSvc1")), + ), + } + }, + }, + { + Description: "build svc0 and svc1", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("composeYaml"), "build", "svc0", "svc1") + }, + + Command: test.Command("images"), + + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.Contains(data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")), + } + }, + }, + { + Description: "build no arg", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "build") + }, + + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "build bogus", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Labels().Get("composeYaml"), + "build", + "svc0", + "svc100", + ) + }, + + Expected: test.Expects(1, []error{errors.New("no such service: svc100")}, nil), + }, + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if data.Labels().Get("imageSvc0") != "" { + helpers.Anyhow("rmi", data.Labels().Get("imageSvc0"), data.Labels().Get("imageSvc1")) + } + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_config_test.go b/cmd/nerdctl/compose/compose_config_test.go index 18dd728da5a..e9f16c6a92d 100644 --- a/cmd/nerdctl/compose/compose_config_test.go +++ b/cmd/nerdctl/compose/compose_config_test.go @@ -18,141 +18,272 @@ package compose import ( "fmt" - "os" - "path/filepath" "testing" "gotest.tools/v3/assert" - "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeConfig(t *testing.T) { - base := testutil.NewBase(t) - - var dockerComposeYAML = ` + const dockerComposeYAML = ` services: hello: image: alpine:3.13 ` + testCase := nerdtest.Setup() - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - base.ComposeCmd("-f", comp.YAMLFullPath(), "config").AssertOutContains("hello:") -} - -func TestComposeConfigWithPrintService(t *testing.T) { - base := testutil.NewBase(t) - - var dockerComposeYAML = ` -services: - hello1: - image: alpine:3.13 -` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + } - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + testCase.SubTests = []*test.Case{ + { + Description: "config contains service name", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "config") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hello:")), + }, + { + Description: "config --services is exactly service name", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Labels().Get("composeYaml"), + "config", + "--services", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello\n")), + }, + { + Description: "config --hash=* contains service name", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "config", "--hash=*") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("hello")), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "config", "--services").AssertOutExactly("hello1\n") + testCase.Run(t) } func TestComposeConfigWithPrintServiceHash(t *testing.T) { - base := testutil.NewBase(t) - - var dockerComposeYAML = ` + const dockerComposeYAML = ` services: - hello1: + hello: image: alpine:%s ` + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(fmt.Sprintf(dockerComposeYAML, "3.13"), "compose.yaml") + + hash := helpers.Capture( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "config", + "--hash=hello", + ) - comp := testutil.NewComposeDir(t, fmt.Sprintf(dockerComposeYAML, "3.13")) - defer comp.CleanUp() + data.Labels().Set("hash", hash) - // `--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") + data.Temp().Save(fmt.Sprintf(dockerComposeYAML, "3.14"), "compose.yaml") } - hash := base.ComposeCmd("-f", comp.YAMLFullPath(), "config", "--hash=hello1").Out() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "config", + "--hash=hello", + ) + } - newComp := testutil.NewComposeDir(t, fmt.Sprintf(dockerComposeYAML, "3.14")) - defer newComp.CleanUp() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout, info string, t *testing.T) { + assert.Assert(t, data.Labels().Get("hash") != stdout, "hash should be different") + }, + } + } - newHash := base.ComposeCmd("-f", newComp.YAMLFullPath(), "config", "--hash=hello1").Out() - assert.Assert(t, hash != newHash) + testCase.Run(t) } func TestComposeConfigWithMultipleFile(t *testing.T) { - base := testutil.NewBase(t) - - var dockerComposeYAML = ` + const dockerComposeBase = ` services: hello1: image: alpine:3.13 ` - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - comp.WriteFile("docker-compose.test.yml", ` + const dockerComposeTest = ` services: hello2: image: alpine:3.14 -`) - comp.WriteFile("docker-compose.override.yml", ` +` + + const dockerComposeOverride = ` services: hello1: image: alpine:3.14 -`) +` + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeBase, "compose.yaml") + data.Temp().Save(dockerComposeTest, "docker-compose.test.yml") + data.Temp().Save(dockerComposeOverride, "docker-compose.override.yml") + + data.Labels().Set("composeDir", data.Temp().Path()) + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + data.Labels().Set("composeYamlTest", data.Temp().Path("docker-compose.test.yml")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "config override", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", data.Labels().Get("composeYaml"), + "-f", data.Labels().Get("composeYamlTest"), + "config", + ) + }, + Expected: test.Expects( + expect.ExitCodeSuccess, + nil, + expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), + ), + }, + { + Description: "project dir", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "--project-directory", data.Labels().Get("composeDir"), "config", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("alpine:3.14")), + }, + { + Description: "project dir services", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "--project-directory", data.Labels().Get("composeDir"), "config", "--services", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("hello1\n")), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "-f", filepath.Join(comp.Dir(), "docker-compose.test.yml"), "config").AssertOutContains("alpine:3.14") - base.ComposeCmd("--project-directory", comp.Dir(), "config", "--services").AssertOutExactly("hello1\n") - base.ComposeCmd("--project-directory", comp.Dir(), "config").AssertOutContains("alpine:3.14") + testCase.Run(t) } func TestComposeConfigWithComposeFileEnv(t *testing.T) { - base := testutil.NewBase(t) - - var dockerComposeYAML = ` + const dockerComposeBase = ` services: hello1: image: alpine:3.13 ` - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - comp.WriteFile("docker-compose.test.yml", ` + const dockerComposeTest = ` services: hello2: image: alpine:3.14 -`) +` + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeBase, "compose.yaml") + data.Temp().Save(dockerComposeTest, "docker-compose.test.yml") - base.Env = append(base.Env, "COMPOSE_FILE="+comp.YAMLFullPath()+","+filepath.Join(comp.Dir(), "docker-compose.test.yml"), "COMPOSE_PATH_SEPARATOR=,") + data.Labels().Set("composeDir", data.Temp().Path()) + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + data.Labels().Set("composeYamlTest", data.Temp().Path("docker-compose.test.yml")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "env config", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "config", + ) + cmd.Setenv("COMPOSE_FILE", data.Labels().Get("composeYaml")+","+data.Labels().Get("composeYamlTest")) + cmd.Setenv("COMPOSE_PATH_SEPARATOR", ",") + return cmd + }, + Expected: test.Expects( + expect.ExitCodeSuccess, + nil, + expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), + ), + }, + { + Description: "env with project dir", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "--project-directory", data.Labels().Get("composeDir"), + "config", + ) + cmd.Setenv("COMPOSE_FILE", data.Labels().Get("composeYaml")+","+data.Labels().Get("composeYamlTest")) + cmd.Setenv("COMPOSE_PATH_SEPARATOR", ",") + return cmd + }, + Expected: test.Expects( + expect.ExitCodeSuccess, + nil, + expect.Contains("alpine:3.13", "alpine:3.14", "hello1", "hello2"), + ), + }, + } - 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") + testCase.Run(t) } 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") + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Temp().Save(envFileContent, "env") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", + "-f", data.Temp().Path("compose.yaml"), + "--env-file", data.Temp().Path("env"), + "config", + ) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("image: hello-world")) + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_cp_linux_test.go b/cmd/nerdctl/compose/compose_cp_linux_test.go index 605210d8946..7d5dea8502c 100644 --- a/cmd/nerdctl/compose/compose_cp_linux_test.go +++ b/cmd/nerdctl/compose/compose_cp_linux_test.go @@ -18,18 +18,18 @@ package compose import ( "fmt" - "os" - "path/filepath" "testing" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeCopy(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` version: '3.1' @@ -39,31 +39,54 @@ services: 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) - + const testFileContent = "test-file-content" + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", compYamlPath, "up", "-d") + + srcFilePath := data.Temp().Save(testFileContent, "test-file") + + data.Labels().Set("composeYaml", compYamlPath) + data.Labels().Set("srcFile", srcFilePath) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.SubTests = []*test.Case{ + { + Description: "test copy to service /dest-no-exist-no-slash", + // These are expected to run in sequence + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", + "-f", data.Labels().Get("composeYaml"), + "cp", data.Labels().Get("srcFile"), "svc0:/dest-no-exist-no-slash") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "test copy from service test-file2", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", + "-f", data.Labels().Get("composeYaml"), + "cp", "svc0:/dest-no-exist-no-slash", data.Temp().Path("test-file2")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + copied := data.Temp().Load("test-file2") + assert.Equal(t, copied, testFileContent) + }, + } + }, + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_create_linux_test.go b/cmd/nerdctl/compose/compose_create_linux_test.go index 43f2dc40067..58e92514b82 100644 --- a/cmd/nerdctl/compose/compose_create_linux_test.go +++ b/cmd/nerdctl/compose/compose_create_linux_test.go @@ -18,40 +18,73 @@ package compose import ( "fmt" + "strings" "testing" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeCreate(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s `, testutil.AlpineImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // 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", "-a").AssertOutContainsAny("Created", "created") - // 1.2 created container can be started by `compose start` - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK() + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", compYamlPath) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.SubTests = []*test.Case{ + { + Description: "`compose create` should work", + // These are sequential + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "create") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "`compose create` should have created service container (in `created` status)", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + assert.Assert(t, + strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), + "stdout should contain `created`") + }), + }, + { + Description: "`created container can be started by `compose start`", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "start") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + } + + testCase.Run(t) } func TestComposeCreateDependency(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -61,25 +94,60 @@ services: image: %s `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // `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", "-a").AssertOutContainsAny("Created", "created") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1", "-a").AssertOutContainsAny("Created", "created") + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + compYamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", compYamlPath) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + testCase.SubTests = []*test.Case{ + { + Description: "`compose create` should work", + // These are sequential + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "create") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "`compose create` should have created svc0 (in `created` status)", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc0", "-a") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + assert.Assert(t, + strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), + "stdout should contain `created`") + }), + }, + { + Description: "`compose create` should have created svc1 (in `created` status)", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "ps", "svc1", "-a") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout, info string, t *testing.T) { + assert.Assert(t, + strings.Contains(stdout, "created") || strings.Contains(stdout, "Created"), + "stdout should contain `created`") + }), + }, + } + + testCase.Run(t) } func TestComposeCreatePull(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s diff --git a/cmd/nerdctl/compose/compose_exec_linux_test.go b/cmd/nerdctl/compose/compose_exec_linux_test.go index 0546ebed6f5..0f86c447de4 100644 --- a/cmd/nerdctl/compose/compose_exec_linux_test.go +++ b/cmd/nerdctl/compose/compose_exec_linux_test.go @@ -17,20 +17,23 @@ package compose import ( - "errors" "fmt" "net" + "path/filepath" "strings" "testing" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeExec(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + dockerComposeYAML := fmt.Sprintf(` version: '3.1' services: @@ -42,107 +45,109 @@ services: command: "sleep infinity" `, testutil.CommonImage, 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", "svc0").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - - // test basic functionality and `--workdir` flag - 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) { - 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() - - // FYI: https://github.com/containerd/nerdctl/blob/e4b2b6da56555dc29ed66d0fd8e7094ff2bc002d/cmd/nerdctl/run_test.go#L177 - 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=", - "--env", "QUX", // not exported in OS - "--env", "QUUX=quux1", - "--env", "QUUX=quux2", - "--env", "CORGE", // OS exported - "--env", "GRAULT=grault_key=grault_value", // value contains `=` char - "--env", "GARPLY=", // OS exported - "--env", "WALDO=", // not exported in OS - - "svc0", "env").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "\nFOO=foo1,foo2\n") { - return errors.New("got bad FOO") - } - if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") { - return errors.New("got bad BAR") - } - if !strings.Contains(stdout, "\nBAZ=\n") { - return errors.New("got bad BAZ") - } - if strings.Contains(stdout, "QUX") { - return errors.New("got bad QUX (should not be set)") - } - if !strings.Contains(stdout, "\nQUUX=quux2\n") { - return errors.New("got bad QUUX") - } - if !strings.Contains(stdout, "\nCORGE=corge-value-in-host\n") { - return errors.New("got bad CORGE") - } - if !strings.Contains(stdout, "\nGRAULT=grault_key=grault_value\n") { - return errors.New("got bad GRAULT") - } - if !strings.Contains(stdout, "\nGARPLY=\n") { - return errors.New("got bad GARPLY") - } - if !strings.Contains(stdout, "\nWALDO=\n") { - return errors.New("got bad WALDO") - } - - return nil - }) -} + testCase := nerdtest.Setup() -func TestComposeExecWithUser(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' + testCase.Setup = func(data test.Data, helpers test.Helpers) { + yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("YAMLPath", yamlPath) + helpers.Ensure("compose", "-f", yamlPath, "up", "-d", "svc0") + } -services: - svc0: - image: %s - command: "sleep infinity" -`, testutil.CommonImage) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase.SubTests = []*test.Case{ + { + Description: "exec no tty", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Labels().Get("YAMLPath"), + "exec", + "-i=false", + "--no-TTY", + "svc0", + "echo", + "success", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("success\n")), + }, + { + Description: "exec no tty with workdir", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Labels().Get("YAMLPath"), + "exec", + "-i=false", + "--no-TTY", + "--workdir", + "/tmp", + "svc0", + "pwd", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("/tmp\n")), + }, + { + Description: "cannot exec on non-running service", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "svc1", "echo", "success") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "with env", + Env: map[string]string{ + "CORGE": "corge-value-in-host", + "GARPLY": "garply-value-in-host", + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "compose", + "-f", + data.Labels().Get("YAMLPath"), + "exec", + "-i=false", + "--no-TTY", + "--env", "FOO=foo1,foo2", + "--env", "BAR=bar1 bar2", + "--env", "BAZ=", + "--env", "QUX", // not exported in OS + "--env", "QUUX=quux1", + "--env", "QUUX=quux2", + "--env", "CORGE", // OS exported + "--env", "GRAULT=grault_key=grault_value", // value contains `=` char + "--env", "GARPLY=", // OS exported + "--env", "WALDO=", // not exported in OS + "svc0", + "env") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains( + "\nFOO=foo1,foo2\n", + "\nBAR=bar1 bar2\n", + "\nBAZ=\n", + "\nQUUX=quux2\n", + "\nCORGE=corge-value-in-host\n", + "\nGRAULT=grault_key=grault_value\n", + "\nGARPLY=\n", + "\nWALDO=\n"), + expect.DoesNotContain("QUX"), + )), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + userSubTest := &test.Case{ + Description: "with user", + SubTests: []*test.Case{}, + } - testCases := map[string]string{ + userCasesMap := map[string]string{ "": "uid=0(root) gid=0(root)", "1000": "uid=1000 gid=0(root)", "1000:users": "uid=1000 gid=100(users)", @@ -151,21 +156,29 @@ services: "nobody:users": "uid=65534(nobody) gid=100(users)", } - for userStr, expected := range testCases { - args := []string{"-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY"} - if userStr != "" { - args = append(args, "--user", userStr) - } - args = append(args, "svc0", "id") - base.ComposeCmd(args...).AssertOutContains(expected) + for k, v := range userCasesMap { + userSubTest.SubTests = append(userSubTest.SubTests, &test.Case{ + Description: k + " " + v, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + args := []string{"compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "--no-TTY"} + if k != "" { + args = append(args, "--user", k) + } + args = append(args, "svc0", "id") + return helpers.Command(args...) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(v)), + }) } + + testCase.SubTests = append(testCase.SubTests, userSubTest) + + testCase.Run(t) } func TestComposeExecTTY(t *testing.T) { - // `-i` in `compose run & exec` is only supported in compose v2. - base := testutil.NewBase(t) - - var dockerComposeYAML = fmt.Sprintf(` + const expectedOutput = "speed 38400 baud" + dockerComposeYAML := fmt.Sprintf(` version: '3.1' services: @@ -175,29 +188,85 @@ services: image: %s `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - - testContainer := testutil.Identifier(t) - base.ComposeCmd("-f", comp.YAMLFullPath(), "run", "-d", "-i=false", "--name", testContainer, "svc0", "sleep", "1h").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() - base.EnsureContainerStarted(testContainer) - - const sttyPartialOutput = "speed 38400 baud" - // 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.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", "--no-TTY", "svc0", "stty").AssertFail() // `-i` - base.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), "exec", "-i=false", "--no-TTY", "svc0", "stty").AssertFail() + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("YAMLPath", yamlPath) + helpers.Ensure( + "compose", + "-f", + yamlPath, + "run", + "-d", + "-i=false", + "--name", + data.Identifier(), + "svc0", + "sleep", + "1h", + ) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // FIXME? + // similar, other test does *also* remove the container + helpers.Anyhow("compose", "-f", data.Labels().Get("YAMLPath"), "down", "-v") + } + + testCase.SubTests = []*test.Case{ + { + Description: "stty exec", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "svc0", "stty") + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)), + }, + { + Description: "-i=false stty exec", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "-i=false", "svc0", "stty") + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)), + }, + { + Description: "--no-TTY stty exec", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("compose", "-f", data.Labels().Get("YAMLPath"), "exec", "--no-TTY", "svc0", "stty") + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "-i=false --no-TTY stty exec", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Labels().Get("YAMLPath"), + "exec", + "-i=false", + "--no-TTY", + "svc0", + "stty", + ) + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + } + + testCase.Run(t) } func TestComposeExecWithIndex(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` + dockerComposeYAML := fmt.Sprintf(` version: '3.1' services: @@ -208,39 +277,52 @@ services: 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") + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + yamlPath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("YAMLPath", yamlPath) + data.Labels().Set("projectName", strings.ToLower(filepath.Base(data.Temp().Dir()))) + + helpers.Ensure("compose", "-f", yamlPath, "up", "-d", "svc0") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + } + + for _, index := range []string{"1", "2", "3"} { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: index, + Setup: func(data test.Data, helpers test.Helpers) { + // try 5 times to ensure that results are stable + for range 5 { + cmds := []string{ + "compose", + "-f", + data.Labels().Get("YAMLPath"), + "exec", + "-i=false", + "--no-TTY", + "--index", + index, + "svc0", + } + + hsts := helpers.Capture(append(cmds, "cat", "/etc/hosts")...) + ips := helpers.Capture(append(cmds, "ip", "addr", "show", "dev", "eth0")...) + + var ( + expectIP string + realIP string + ) + name := fmt.Sprintf("%s-svc0-%s", data.Labels().Get("projectName"), index) + host := fmt.Sprintf("%s.%s_default", name, data.Labels().Get("projectName")) + if nerdtest.IsDocker() { + host = strings.TrimSpace(helpers.Capture("ps", "--filter", "name="+name, "--format", "{{.ID}}")) + } + + lines := strings.Split(hsts, "\n") for _, line := range lines { if !strings.Contains(line, host) { continue @@ -250,37 +332,32 @@ services: 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") + + var ip string + lines = strings.Split(ips, "\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 } - 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 + pip := net.ParseIP(ip) + + assert.Assert(helpers.T(), pip != nil, "fail to get the real ip address") + realIP = pip.String() + + assert.Equal(helpers.T(), realIP, expectIP) + } + }, + }) } - return net.ParseIP(ip) + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_images_linux_test.go b/cmd/nerdctl/compose/compose_images_linux_test.go index f9f7f475186..ae4d9f8eb16 100644 --- a/cmd/nerdctl/compose/compose_images_linux_test.go +++ b/cmd/nerdctl/compose/compose_images_linux_test.go @@ -17,24 +17,26 @@ package compose import ( - "encoding/json" "fmt" - "strings" "testing" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" + + "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeImages(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 + container_name: wordpress environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -44,6 +46,7 @@ services: - wordpress:/var/www/html db: image: %s + container_name: db environment: MYSQL_DATABASE: exampledb MYSQL_USER: exampleuser @@ -57,95 +60,71 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - 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() - - wordpressImageName := strings.Split(testutil.WordpressImage, ":")[0] - dbImageName := strings.Split(testutil.MariaDBImage, ":")[0] - - // check one service image - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "db").AssertOutContains(dbImageName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "db").AssertOutNotContains(wordpressImageName) - - // check all service images - base.ComposeCmd("-f", comp.YAMLFullPath(), "images").AssertOutContains(dbImageName) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images").AssertOutContains(wordpressImageName) -} + wordpressImageName, _ := referenceutil.Parse(testutil.WordpressImage) + dbImageName, _ := referenceutil.Parse(testutil.MariaDBImage) -func TestComposeImagesJson(t *testing.T) { - base := testutil.NewBase(t) - var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' + testCase := nerdtest.Setup() -services: - wordpress: - image: %s - container_name: wordpress - 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 - environment: - MYSQL_DATABASE: exampledb - MYSQL_USER: exampleuser - MYSQL_PASSWORD: examplepass - MYSQL_RANDOM_ROOT_PASSWORD: '1' - volumes: - - db:/var/lib/mysql - -volumes: - wordpress: - db: -`, testutil.WordpressImage, testutil.MariaDBImage) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + data.Labels().Set("composeYaml", data.Temp().Path("compose.yaml")) + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + } - 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(svc string, count int, fields ...string) func(stdout string) error { - return func(stdout string) error { - // 1. check json output can be unmarshalled back to printables. - var printables []composeContainerPrintable - if err := json.Unmarshal([]byte(stdout), &printables); err != nil { - return fmt.Errorf("[service: %s]failed to unmarshal json output from `compose images`: %s", svc, stdout) - } - // 2. check #printables matches expected count. - if len(printables) != count { - return fmt.Errorf("[service: %s]unmarshal generates %d printables, expected %d: %s", svc, len(printables), count, stdout) - } - // 3. check marshalled json string has all expected substrings. - for _, field := range fields { - if !strings.Contains(stdout, field) { - return fmt.Errorf("[service: %s]marshalled json output doesn't have expected string (%s): %s", svc, field, stdout) - } - } - return nil - } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") } - // check other formats are not supported - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "yaml").AssertFail() - // check all services are up (can be marshalled and unmarshalled) - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "json"). - AssertOutWithFunc(assertHandler("all", 2, `"ContainerName":"wordpress"`, `"ContainerName":"db"`)) + testCase.SubTests = []*test.Case{ + { + Description: "images db", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "db") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains(dbImageName.Name()), + expect.DoesNotContain(wordpressImageName.Name()), + )), + }, + { + Description: "images", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(dbImageName.Name(), wordpressImageName.Name())), + }, + { + Description: "images --format yaml", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "yaml") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "images --format json", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + assert.Equal(t, len(printables), 2) + }), + expect.Contains(`"ContainerName":"wordpress"`, `"ContainerName":"db"`), + )), + }, + { + Description: "images --format json wordpress", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("composeYaml"), "images", "--format", "json", "wordpress") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.JSON([]composeContainerPrintable{}, func(printables []composeContainerPrintable, s string, t tig.T) { + assert.Equal(t, len(printables), 1) + }), + expect.Contains(`"ContainerName":"wordpress"`), + )), + }, + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "--format", "json", "wordpress"). - AssertOutWithFunc(assertHandler("wordpress", 1, `"ContainerName":"wordpress"`)) + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_ps.go b/cmd/nerdctl/compose/compose_ps.go index c3ea4fd2578..badee1755b9 100644 --- a/cmd/nerdctl/compose/compose_ps.go +++ b/cmd/nerdctl/compose/compose_ps.go @@ -21,7 +21,6 @@ import ( "fmt" "strings" "text/tabwriter" - "time" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -345,10 +344,6 @@ func formatPublishers(labelMap map[string]string) []PortPublisher { // 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. diff --git a/cmd/nerdctl/compose/compose_pull_linux_test.go b/cmd/nerdctl/compose/compose_pull_linux_test.go index 64e267baa24..e0c79325326 100644 --- a/cmd/nerdctl/compose/compose_pull_linux_test.go +++ b/cmd/nerdctl/compose/compose_pull_linux_test.go @@ -26,14 +26,10 @@ import ( func TestComposePullWithService(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_restart_linux_test.go b/cmd/nerdctl/compose/compose_restart_linux_test.go index 6d5fe1fdedc..1e2ca2c5c61 100644 --- a/cmd/nerdctl/compose/compose_restart_linux_test.go +++ b/cmd/nerdctl/compose/compose_restart_linux_test.go @@ -26,13 +26,9 @@ import ( func TestComposeRestart(t *testing.T) { base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser diff --git a/cmd/nerdctl/compose/compose_rm_linux_test.go b/cmd/nerdctl/compose/compose_rm_linux_test.go index 948ea9e119d..58d149693a9 100644 --- a/cmd/nerdctl/compose/compose_rm_linux_test.go +++ b/cmd/nerdctl/compose/compose_rm_linux_test.go @@ -18,23 +18,22 @@ package compose import ( "fmt" + "regexp" "testing" - "time" + + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeRemove(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -58,27 +57,71 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - 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() - - // no stopped containers - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") - // remove one stopped service - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutNotContains("wordpress") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running") - // remove all services with `--stop` - base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "-s").AssertOK() - time.Sleep(3 * time.Second) - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutNotContains("db") + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.SubTests = []*test.Case{ + { + Description: "All services are still up", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + comp := expect.Match(regexp.MustCompile("Up|running")) + comp(wp, "", t) + comp(db, "", t) + }, + } + }, + }, + { + Description: "Remove stopped service", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "wordpress") + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + wp := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + expect.DoesNotContain("wordpress")(wp, "", t) + expect.Match(regexp.MustCompile("Up|running"))(db, "", t) + }, + } + }, + }, + { + Description: "Remove all services with stop", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "rm", "-f", "-s") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + db := helpers.Capture("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db") + expect.DoesNotContain("db")(db, "", t) + }, + } + }, + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index 65b36e7ffb6..bd606299bfe 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -26,48 +26,18 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/log" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "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/testregistry" ) func TestComposeRun(t *testing.T) { - base := testutil.NewBase(t) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. - containerName := testutil.Identifier(t) - - dockerComposeYAML := fmt.Sprintf(` -version: '3.1' -services: - alpine: - image: %s - entrypoint: - - stty -`, testutil.AlpineImage) - - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const sttyPartialOutput = "speed 38400 baud" - // 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.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--name", containerName, "alpine").AssertOutContains(sttyPartialOutput) -} - -func TestComposeRunWithRM(t *testing.T) { - base := testutil.NewBase(t) - // specify the name of container in order to remove - // TODO: when `compose rm` is implemented, replace it. - containerName := testutil.Identifier(t) + const expectedOutput = "speed 38400 baud" dockerComposeYAML := fmt.Sprintf(` version: '3.1' @@ -78,29 +48,69 @@ services: - stty `, testutil.AlpineImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() - - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const sttyPartialOutput = "speed 38400 baud" - // 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.ComposeCmdWithHelper(unbuffer, "-f", comp.YAMLFullPath(), - "run", "--name", containerName, "--rm", "alpine").AssertOutContains(sttyPartialOutput) - - psCmd := base.Cmd("ps", "-a", "--format=\"{{.Names}}\"") - result := psCmd.Run() - stdoutContent := result.Stdout() + result.Stderr() - assert.Assert(psCmd.Base.T, result.ExitCode == 0, stdoutContent) - if strings.Contains(stdoutContent, containerName) { - log.L.Errorf("test failed, the container %s is not removed", stdoutContent) - t.Fail() - return + testCase := nerdtest.Setup() + + testCase.SubTests = []*test.Case{ + { + Description: "pty run", + Setup: func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(0, nil, expect.Contains(expectedOutput)), + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + }, + }, + { + Description: "pty run with --rm", + Setup: func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--name", + data.Identifier(), + "--rm", + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + // Ensure the container has been removed + capt := helpers.Capture("ps", "-a", "--format=\"{{.Names}}\"") + assert.Assert(t, !strings.Contains(capt, data.Identifier()), capt) + + return &test.Expected{ + Output: expect.Contains(expectedOutput), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", "-v", data.Identifier()) + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down", "-v") + }, + }, } + + testCase.Run(t) } func TestComposeRunWithServicePorts(t *testing.T) { diff --git a/cmd/nerdctl/compose/compose_start.go b/cmd/nerdctl/compose/compose_start.go index 3762753d553..c945f52adb7 100644 --- a/cmd/nerdctl/compose/compose_start.go +++ b/cmd/nerdctl/compose/compose_start.go @@ -112,7 +112,7 @@ func startContainers(ctx context.Context, client *containerd.Client, containers } // in compose, always disable attach - if err := containerutil.Start(ctx, c, false, client, ""); err != nil { + if err := containerutil.Start(ctx, c, false, false, client, ""); err != nil { return err } info, err := c.Info(ctx, containerd.WithoutRefreshedMetadata) diff --git a/cmd/nerdctl/compose/compose_start_linux_test.go b/cmd/nerdctl/compose/compose_start_linux_test.go index 11c1581cd92..bfb001ad5c9 100644 --- a/cmd/nerdctl/compose/compose_start_linux_test.go +++ b/cmd/nerdctl/compose/compose_start_linux_test.go @@ -18,16 +18,18 @@ package compose import ( "fmt" + "regexp" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStart(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -37,50 +39,68 @@ services: command: "sleep infinity" `, testutil.CommonImage, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } - // calling `compose start` after all services up has no effect. - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "start") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "stop", "--timeout", "1", "svc0") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "kill", "svc1") + } - // `compose start`` can start a stopped/killed service container - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "--timeout", "1", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "svc1").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Up", "running") - base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Up", "running") -} + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") + } -func TestComposeStartFailWhenServicePause(t *testing.T) { - base := testutil.NewBase(t) - switch base.Info().CgroupDriver { - case "none", "": - t.Skip("requires cgroup (for pausing)") + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: nil, + Output: func(stdout, info string, t *testing.T) { + svc0 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc0") + svc1 := helpers.Capture("compose", "-f", data.Temp().Path("compose.yaml"), "ps", "svc1") + comp := expect.Match(regexp.MustCompile("Up|running")) + comp(svc0, "", t) + comp(svc1, "", t) + }, + } } - var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' + testCase.Run(t) +} +func TestComposeStartFailWhenServicePause(t *testing.T) { + var dockerComposeYAML = fmt.Sprintf(` 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) + testCase := nerdtest.Setup() + + testCase.Require = nerdtest.CGroup + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "pause", "svc0") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "start") + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, nil, nil) - // `compose start` cannot start a paused service container - base.ComposeCmd("-f", comp.YAMLFullPath(), "pause", "svc0").AssertOK() - base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertFail() + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_stop_linux_test.go b/cmd/nerdctl/compose/compose_stop_linux_test.go index e10b16ff7b2..ac346b90507 100644 --- a/cmd/nerdctl/compose/compose_stop_linux_test.go +++ b/cmd/nerdctl/compose/compose_stop_linux_test.go @@ -18,22 +18,22 @@ package compose import ( "fmt" + "regexp" "testing" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeStop(t *testing.T) { - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: wordpress: image: %s - ports: - - 8080:80 environment: WORDPRESS_DB_HOST: db WORDPRESS_DB_USER: exampleuser @@ -57,21 +57,50 @@ volumes: db: `, testutil.WordpressImage, testutil.MariaDBImage) - 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() - - // stop should (only) stop the given service. - base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK() - 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", "-a").AssertOutContainsAny("Exit", "exited") - + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.SubTests = []*test.Case{ + { + Description: "stop db", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "db") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "db", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), + }, + { + Description: "wordpress is still running", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Up|running"))), + }, + { + Description: "stop wordpress", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("compose", "-f", data.Labels().Get("yamlPath"), "stop", "--timeout", "5", "wordpress") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "ps", "wordpress", "-a") + }, + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("Exit|exited"))), + }, + } + + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_top_linux_test.go b/cmd/nerdctl/compose/compose_top_linux_test.go index a0474c51b0b..9620aa113c1 100644 --- a/cmd/nerdctl/compose/compose_top_linux_test.go +++ b/cmd/nerdctl/compose/compose_top_linux_test.go @@ -20,20 +20,16 @@ import ( "fmt" "testing" - "github.com/containerd/nerdctl/v2/pkg/infoutil" - "github.com/containerd/nerdctl/v2/pkg/rootlessutil" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeTop(t *testing.T) { - if rootlessutil.IsRootless() && infoutil.CgroupsVersion() == "1" { - t.Skip("test skipped for rootless containers on cgroup v1") - } - - base := testutil.NewBase(t) var dockerComposeYAML = fmt.Sprintf(` -version: '3.1' - services: svc0: image: %s @@ -42,15 +38,36 @@ services: image: %s `, testutil.CommonImage, testutil.NginxAlpineImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - projectName := comp.ProjectName() - t.Logf("projectName=%q", projectName) + testCase := nerdtest.Setup() + + testCase.Require = require.All(nerdtest.CgroupsAccessible) - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") + helpers.Ensure("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") + data.Labels().Set("yamlPath", data.Temp().Path("compose.yaml")) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } + + testCase.SubTests = []*test.Case{ + { + Description: "svc0 contains sleep infinity", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc0") + }, + Expected: test.Expects(0, nil, expect.Contains("sleep infinity")), + }, + { + Description: "svc1 contains sleep nginx", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Labels().Get("yamlPath"), "top", "svc1") + }, + Expected: test.Expects(0, nil, expect.Contains("nginx")), + }, + } - // a running container should have the process command in output - base.ComposeCmd("-f", comp.YAMLFullPath(), "top", "svc0").AssertOutContains("sleep infinity") - base.ComposeCmd("-f", comp.YAMLFullPath(), "top", "svc1").AssertOutContains("nginx") + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_up_linux_test.go b/cmd/nerdctl/compose/compose_up_linux_test.go index 6da3162b4b9..3d7597adf88 100644 --- a/cmd/nerdctl/compose/compose_up_linux_test.go +++ b/cmd/nerdctl/compose/compose_up_linux_test.go @@ -19,6 +19,7 @@ package compose import ( "fmt" "io" + "os" "strings" "testing" "time" @@ -499,6 +500,12 @@ func TestComposeUpProfile(t *testing.T) { serviceRegular := testutil.Identifier(t) + "-regular" serviceProfiled := testutil.Identifier(t) + "-profiled" + // write the env.common file to tmpdir + tmpDir := t.TempDir() + envFilePath := fmt.Sprintf("%s/env.common", tmpDir) + err := os.WriteFile(envFilePath, []byte("TEST_ENV_INJECTION=WORKS\n"), 0644) + assert.NilError(t, err) + dockerComposeYAML := fmt.Sprintf(` services: %s: @@ -508,7 +515,9 @@ services: image: %[3]s profiles: - test-profile -`, serviceRegular, serviceProfiled, testutil.NginxAlpineImage) + env_file: + - %[4]s +`, serviceRegular, serviceProfiled, testutil.NginxAlpineImage, envFilePath) // * Test with profile // Should run both the services: @@ -521,6 +530,10 @@ services: psCmd := base.Cmd("ps", "-a", "--format={{.Names}}") psCmd.AssertOutContains(serviceRegular) psCmd.AssertOutContains(serviceProfiled) + + execCmd := base.ComposeCmd("-f", comp1.YAMLFullPath(), "exec", serviceProfiled, "env") + execCmd.AssertOutContains("TEST_ENV_INJECTION=WORKS") + base.ComposeCmd("-f", comp1.YAMLFullPath(), "--profile", "test-profile", "down", "-v").AssertOK() // * Test without profile diff --git a/cmd/nerdctl/compose/compose_up_test.go b/cmd/nerdctl/compose/compose_up_test.go index 63bba829fcf..48f1b5c688f 100644 --- a/cmd/nerdctl/compose/compose_up_test.go +++ b/cmd/nerdctl/compose/compose_up_test.go @@ -17,69 +17,88 @@ package compose import ( + "errors" "fmt" - "os" - "path/filepath" - "runtime" "testing" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" + + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // https://github.com/containerd/nerdctl/issues/1942 func TestComposeUpDetailedError(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("FIXME: test does not work on Windows yet (runtime \"io.containerd.runc.v2\" binary not installed \"containerd-shim-runc-v2.exe\": file does not exist)") - } - base := testutil.NewBase(t) dockerComposeYAML := fmt.Sprintf(` services: foo: image: %s runtime: invalid `, testutil.CommonImage) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - c := base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d") - expected := icmd.Expected{ - ExitCode: 1, - Err: `exec: \"invalid\": executable file not found in $PATH`, + testCase := nerdtest.Setup() + + // "FIXME: test does not work on Windows yet (runtime \"io.containerd.runc.v2\" binary not installed \"containerd-shim-runc-v2.exe\": file does not exist) + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerComposeYAML, "compose.yaml") } - if base.Target == testutil.Docker { - expected.Err = `unknown or invalid runtime name: invalid` + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up", "-d") } - c.Assert(expected) + + testCase.Expected = test.Expects( + 1, + []error{errors.New(`invalid runtime name`)}, + nil, + ) + + testCase.Run(t) } // https://github.com/containerd/nerdctl/issues/1652 func TestComposeUpBindCreateHostPath(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip(`FIXME: no support for Windows path: (error: "volume target must be an absolute path, got \"/mnt\")`) - } + testCase := nerdtest.Setup() - base := testutil.NewBase(t) + // `FIXME: no support for Windows path: (error: "volume target must be an absolute path, got \"/mnt\")` + testCase.Require = require.Not(require.Windows) - var dockerComposeYAML = fmt.Sprintf(` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + var dockerComposeYAML = fmt.Sprintf(` services: test: image: %s command: sh -euxc "echo hi >/mnt/test" volumes: - # ./foo should be automatically created - - ./foo:/mnt -`, testutil.CommonImage) + # tempdir/foo should be automatically created + - %s:/mnt +`, testutil.CommonImage, data.Temp().Path("foo")) + + data.Temp().Save(dockerComposeYAML, "compose.yaml") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("compose", "-f", data.Temp().Path("compose.yaml"), "up") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("compose", "-f", data.Temp().Path("compose.yaml"), "down") + } - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Errors: nil, + Output: func(stdout, info string, t *testing.T) { + assert.Equal(t, data.Temp().Load("foo", "test"), "hi\n") + }, + } + } - base.ComposeCmd("-f", comp.YAMLFullPath(), "up").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down").AssertOK() - testFile := filepath.Join(comp.Dir(), "foo", "test") - testB, err := os.ReadFile(testFile) - assert.NilError(t, err) - assert.Equal(t, "hi\n", string(testB)) + testCase.Run(t) } diff --git a/cmd/nerdctl/compose/compose_version_test.go b/cmd/nerdctl/compose/compose_version_test.go index af3028b3d65..04cdd244052 100644 --- a/cmd/nerdctl/compose/compose_version_test.go +++ b/cmd/nerdctl/compose/compose_version_test.go @@ -19,20 +19,29 @@ package compose import ( "testing" - "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestComposeVersion(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version").AssertOutContains("Compose version ") + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version") + testCase.Expected = test.Expects(0, nil, expect.Contains("Compose version ")) + testCase.Run(t) } func TestComposeVersionShort(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version", "--short").AssertOK() + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version", "--short") + testCase.Expected = test.Expects(0, nil, nil) + testCase.Run(t) } func TestComposeVersionJson(t *testing.T) { - base := testutil.NewBase(t) - base.ComposeCmd("version", "--format", "json").AssertOutContains("{\"version\":\"") + testCase := nerdtest.Setup() + testCase.Command = test.Command("compose", "version", "--format", "json") + testCase.Expected = test.Expects(0, nil, expect.Contains("{\"version\":\"")) + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_attach.go b/cmd/nerdctl/container/container_attach.go index 958c7c4b7ec..5fd004ae36e 100644 --- a/cmd/nerdctl/container/container_attach.go +++ b/cmd/nerdctl/container/container_attach.go @@ -17,6 +17,8 @@ package container import ( + "io" + "github.com/spf13/cobra" containerd "github.com/containerd/containerd/v2/client" @@ -56,6 +58,7 @@ Caveats: SilenceErrors: true, } cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") + cmd.Flags().Bool("no-stdin", false, "Do not attach STDIN") return cmd } @@ -68,9 +71,18 @@ func attachOptions(cmd *cobra.Command) (types.ContainerAttachOptions, error) { if err != nil { return types.ContainerAttachOptions{}, err } + noStdin, err := cmd.Flags().GetBool("no-stdin") + if err != nil { + return types.ContainerAttachOptions{}, err + } + + var stdin io.Reader + if !noStdin { + stdin = cmd.InOrStdin() + } return types.ContainerAttachOptions{ GOptions: globalOptions, - Stdin: cmd.InOrStdin(), + Stdin: stdin, Stdout: cmd.OutOrStdout(), Stderr: cmd.ErrOrStderr(), DetachKeys: detachKeys, diff --git a/cmd/nerdctl/container/container_attach_linux_test.go b/cmd/nerdctl/container/container_attach_linux_test.go index d2f5a7b7b0d..88ab8bb5430 100644 --- a/cmd/nerdctl/container/container_attach_linux_test.go +++ b/cmd/nerdctl/container/container_attach_linux_test.go @@ -17,8 +17,9 @@ package container import ( + "bytes" "errors" - "os" + "io" "strings" "testing" "time" @@ -56,11 +57,9 @@ func TestAttach(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "--rm", "-it", "--name", data.Identifier(), testutil.CommonImage) - cmd.WithPseudoTTY(func(f *os.File) error { - // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{16, 17}) - return err - }) + cmd.WithPseudoTTY() + // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) + cmd.Feed(bytes.NewReader([]byte{16, 17})) cmd.Run(&test.Expected{ ExitCode: 0, @@ -74,15 +73,15 @@ func TestAttach(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", data.Identifier()) - cmd.WithPseudoTTY(func(f *os.File) error { - _, _ = f.WriteString("echo mark${NON}mark\n") + + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) + cmd.WithFeeder(func() io.Reader { // Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the // container can read stdin before we detach time.Sleep(time.Second) // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{16, 17}) - - return err + return bytes.NewReader([]byte{16, 17}) }) return cmd @@ -120,10 +119,8 @@ func TestAttachDetachKeys(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-q", "--name", data.Identifier(), testutil.CommonImage) - cmd.WithPseudoTTY(func(f *os.File) error { - _, err := f.Write([]byte{17}) - return err - }) + cmd.WithPseudoTTY() + cmd.Feed(bytes.NewReader([]byte{17})) cmd.Run(&test.Expected{ ExitCode: 0, @@ -137,15 +134,14 @@ func TestAttachDetachKeys(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) - cmd.WithPseudoTTY(func(f *os.File) error { - _, _ = f.WriteString("echo mark${NON}mark\n") + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) + cmd.WithFeeder(func() io.Reader { // Interestingly, and unlike with run, on attach, docker (like nerdctl) ALSO needs a pause so that the // container can read stdin before we detach time.Sleep(time.Second) - // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{1, 2}) - - return err + // ctrl+p and ctrl+q (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) + return bytes.NewReader([]byte{1, 2}) }) return cmd @@ -179,11 +175,9 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { testCase.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) - cmd.WithPseudoTTY(func(f *os.File) error { - // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{1, 2}) - return err - }) + cmd.WithPseudoTTY() + // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) + cmd.Feed(bytes.NewReader([]byte{1, 2})) cmd.Run(&test.Expected{ ExitCode: 0, @@ -197,10 +191,8 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("attach", data.Identifier()) - cmd.WithPseudoTTY(func(f *os.File) error { - _, err := f.WriteString("echo mark${NON}mark\nexit 42\n") - return err - }) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo mark${NON}mark\nexit 42\n")) return cmd } @@ -219,3 +211,44 @@ func TestAttachForAutoRemovedContainer(t *testing.T) { testCase.Run(t) } + +func TestAttachNoStdin(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("run", "-it", "--detach-keys=ctrl-p,ctrl-q", "--name", data.Identifier(), + testutil.CommonImage, "sleep", "5") + cmd.WithPseudoTTY() + cmd.Feed(bytes.NewReader([]byte{16, 17})) // Ctrl-p, Ctrl-q to detach (https://en.wikipedia.org/wiki/C0_and_C1_control_codes) + cmd.Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.State.Running}}", data.Identifier()), "true")) + }, + }) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("attach", "--no-stdin", data.Identifier()) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("should-not-appear\n")) + cmd.Feed(bytes.NewReader([]byte{16, 17})) + return cmd + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, // Since it's a normal exit and not detach. + Output: func(stdout string, info string, t *testing.T) { + logs := helpers.Capture("logs", data.Identifier()) + assert.Assert(t, !strings.Contains(logs, "should-not-appear")) + }, + } + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_commit_linux_test.go b/cmd/nerdctl/container/container_commit_linux_test.go index 8da0ee01b4f..a2b26dbd88e 100644 --- a/cmd/nerdctl/container/container_commit_linux_test.go +++ b/cmd/nerdctl/container/container_commit_linux_test.go @@ -45,7 +45,7 @@ func TestKubeCommitSave(t *testing.T) { containerID = strings.TrimPrefix(stdout, "containerd://") }, }) - data.Set("containerID", containerID) + data.Labels().Set("containerID", containerID) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -53,8 +53,8 @@ func TestKubeCommitSave(t *testing.T) { } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - helpers.Ensure("commit", data.Get("containerID"), "testcommitsave") - return helpers.Command("save", "testcommitsave") + helpers.Ensure("commit", data.Labels().Get("containerID"), data.Identifier("testcommitsave")) + return helpers.Command("save", data.Identifier("testcommitsave")) } testCase.Expected = test.Expects(0, nil, nil) diff --git a/cmd/nerdctl/container/container_cp_acid_linux_test.go b/cmd/nerdctl/container/container_cp_acid_linux_test.go index fc30c4ab314..c5a92ee70bd 100644 --- a/cmd/nerdctl/container/container_cp_acid_linux_test.go +++ b/cmd/nerdctl/container/container_cp_acid_linux_test.go @@ -28,6 +28,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // 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 @@ -87,7 +88,7 @@ func TestCopyAcid(t *testing.T) { setup() expectedErr := containerutil.ErrTargetIsReadOnly.Error() - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { expectedErr = "" } diff --git a/cmd/nerdctl/container/container_cp_linux_test.go b/cmd/nerdctl/container/container_cp_linux_test.go index a584b722243..1a268caa60e 100644 --- a/cmd/nerdctl/container/container_cp_linux_test.go +++ b/cmd/nerdctl/container/container_cp_linux_test.go @@ -30,6 +30,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) // For the test matrix, see https://docs.docker.com/engine/reference/commandline/cp/ @@ -898,7 +899,7 @@ func cpTestHelper(t *testing.T, tg *testgroup) { setup() // If Docker, removes the err part of expectation - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { testCase.expect.Err = "" } @@ -938,7 +939,7 @@ func cpTestHelper(t *testing.T, tg *testgroup) { cmd = base.Cmd("cp", containerStopped+":"+sourceSpec, destinationSpec) } - if rootlessutil.IsRootless() && testutil.GetTarget() == testutil.Nerdctl { + if rootlessutil.IsRootless() && !nerdtest.IsDocker() { cmd.Assert( icmd.Expected{ ExitCode: 1, diff --git a/cmd/nerdctl/container/container_cp_freebsd.go b/cmd/nerdctl/container/container_cp_nolinux.go similarity index 97% rename from cmd/nerdctl/container/container_cp_freebsd.go rename to cmd/nerdctl/container/container_cp_nolinux.go index 4e7d2cfd518..95a9accec0a 100644 --- a/cmd/nerdctl/container/container_cp_freebsd.go +++ b/cmd/nerdctl/container/container_cp_nolinux.go @@ -1,3 +1,5 @@ +//go:build !linux + /* Copyright The containerd Authors. diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index 1ef0f958f9c..e8d7e6a4d33 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -21,6 +21,7 @@ import ( "runtime" "github.com/spf13/cobra" + cdiparser "tags.cncf.io/container-device-interface/pkg/parser" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/api/types" @@ -55,6 +56,7 @@ func CreateCommand() *cobra.Command { return cmd } +//revive:disable:function-length func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { var err error opt := types.ContainerCreateOptions{ @@ -159,6 +161,14 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { if err != nil { return opt, err } + opt.CPURealtimePeriod, err = cmd.Flags().GetUint64("cpu-rt-period") + if err != nil { + return opt, err + } + opt.CPURealtimeRuntime, err = cmd.Flags().GetUint64("cpu-rt-runtime") + if err != nil { + return opt, err + } opt.Memory, err = cmd.Flags().GetString("memory") if err != nil { return opt, err @@ -199,19 +209,50 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { if err != nil { return opt, err } + opt.Cgroupns, err = cmd.Flags().GetString("cgroupns") + if err != nil { + return opt, err + } + opt.CgroupParent, err = cmd.Flags().GetString("cgroup-parent") + if err != nil { + return opt, err + } + + allDevices, err := cmd.Flags().GetStringSlice("device") + if err != nil { + return opt, err + } + for _, device := range allDevices { + if cdiparser.IsQualifiedName(device) { + opt.CDIDevices = append(opt.CDIDevices, device) + } else { + opt.Device = append(opt.Device, device) + } + } + // #endregion + + // #region for blkio flags opt.BlkioWeight, err = cmd.Flags().GetUint16("blkio-weight") if err != nil { return opt, err } - opt.Cgroupns, err = cmd.Flags().GetString("cgroupns") + opt.BlkioWeightDevice, err = cmd.Flags().GetStringArray("blkio-weight-device") if err != nil { return opt, err } - opt.CgroupParent, err = cmd.Flags().GetString("cgroup-parent") + opt.BlkioDeviceReadBps, err = cmd.Flags().GetStringArray("device-read-bps") + if err != nil { + return opt, err + } + opt.BlkioDeviceWriteBps, err = cmd.Flags().GetStringArray("device-write-bps") if err != nil { return opt, err } - opt.Device, err = cmd.Flags().GetStringSlice("device") + opt.BlkioDeviceReadIOps, err = cmd.Flags().GetStringArray("device-read-iops") + if err != nil { + return opt, err + } + opt.BlkioDeviceWriteIOps, err = cmd.Flags().GetStringArray("device-write-iops") if err != nil { return opt, err } @@ -423,6 +464,30 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { } // #endregion + // #region for UserNS + opt.UserNS, err = cmd.Flags().GetString("userns-remap") + if err != nil { + return opt, err + } + + userns, err := cmd.Flags().GetString("userns") + if err != nil { + return opt, err + } + + if userns == "host" { + opt.UserNS = "" + } else if userns != "" { + return opt, fmt.Errorf("invalid user mode") + } + + if opt.Privileged && opt.UserNS != "" { + //userns-remap is not supported with privileged flag. + // Ref: https://docs.docker.com/engine/security/userns-remap/ + return opt, fmt.Errorf("privileged flag cannot be used with userns-remap") + } + // #endregion + return opt, nil } diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 0a20ddac9c2..66da8a19e86 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -19,15 +19,19 @@ package container import ( "errors" "fmt" + "io" "os" "path/filepath" + "strconv" "strings" + "syscall" "testing" "github.com/opencontainers/go-digest" "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/defaults" + "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" @@ -126,7 +130,7 @@ func TestCreateWithMACAddress(t *testing.T) { 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 && + if nerdtest.IsDocker() && (network == networkIPvlan || network == "container:whatever"+tID) { // unlike nerdctl // when using network ipvlan or container in Docker @@ -137,7 +141,7 @@ func TestCreateWithMACAddress(t *testing.T) { } // See https://github.com/containerd/nerdctl/issues/3101 - if testutil.GetTarget() == testutil.Docker && + if nerdtest.IsDocker() && (network == networkBridge) { expect = "" } @@ -199,7 +203,7 @@ func TestIssue2993(t *testing.T) { { 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() + dataRoot := data.Temp().Path() helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity) @@ -218,25 +222,25 @@ func TestIssue2993(t *testing.T) { assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) - data.Set(containersPathKey, containersPath) - data.Set(etchostsPathKey, etchostsPath) + data.Labels().Set(containersPathKey, containersPath) + data.Labels().Set(etchostsPathKey, etchostsPath) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "--data-root", data.TempDir(), "-f", data.Identifier()) + helpers.Anyhow("rm", "--data-root", data.Temp().Path(), "-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) + return helpers.Command("run", "--data-root", data.Temp().Path(), "--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)) + containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 1) - etchostsDirs, err := os.ReadDir(data.Get(etchostsPathKey)) + etchostsDirs, err := os.ReadDir(data.Labels().Get(etchostsPathKey)) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) }, @@ -246,7 +250,7 @@ func TestIssue2993(t *testing.T) { { 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() + dataRoot := data.Temp().Path() helpers.Ensure("run", "--data-root", dataRoot, "--name", data.Identifier(), "-d", testutil.AlpineImage, "sleep", nerdtest.Infinity) @@ -265,25 +269,25 @@ func TestIssue2993(t *testing.T) { assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 1) - data.Set(containersPathKey, containersPath) - data.Set(etchostsPathKey, etchostsPath) + data.Labels().Set(containersPathKey, containersPath) + data.Labels().Set(etchostsPathKey, etchostsPath) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("--data-root", data.TempDir(), "rm", "-f", data.Identifier()) + helpers.Anyhow("--data-root", data.Temp().Path(), "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()) + return helpers.Command("--data-root", data.Temp().Path(), "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)) + containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) assert.NilError(t, err) assert.Equal(t, len(containersDirs), 0) - etchostsDirs, err := os.ReadDir(data.Get(etchostsPathKey)) + etchostsDirs, err := os.ReadDir(data.Labels().Get(etchostsPathKey)) assert.NilError(t, err) assert.Equal(t, len(etchostsDirs), 0) }, @@ -325,3 +329,184 @@ func TestCreateFromOCIArchive(t *testing.T) { 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") } + +func TestUsernsMappingCreateCmd(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.All( + nerdtest.AllowModifyUserns, + nerdtest.RemapIDs, + require.Not(nerdtest.Docker)), + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set("validUserns", "nerdctltestuser") + data.Labels().Set("expectedHostUID", "123456789") + data.Labels().Set("invalidUserns", "invaliduser") + }, + SubTests: []*test.Case{ + { + Description: "Test container create with valid Userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("create", "--tty", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + return helpers.Command("start", 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) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + assert.NilError(t, err, "Failed to get container host UID") + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container create failure with valid Userns and privileged flag", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "Test container create with invalid Userns", + NoParallel: true, // Changes system config so running in non parallel mode + 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("create", "--tty", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.NginxAlpineImage) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + }, + } + testCase.Run(t) +} + +func getContainerHostUID(helpers test.Helpers, containerName string) (string, error) { + result := helpers.Capture("inspect", "--format", "{{.State.Pid}}", containerName) + pidStr := strings.TrimSpace(result) + pid, err := strconv.Atoi(pidStr) + if err != nil { + return "", fmt.Errorf("invalid PID: %v", err) + } + + stat, err := os.Stat(fmt.Sprintf("/proc/%d", pid)) + if err != nil { + return "", fmt.Errorf("failed to stat process: %v", err) + } + + uid := int(stat.Sys().(*syscall.Stat_t).Uid) + return strconv.Itoa(uid), nil +} + +func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) error { + addUser(userns, hostUID, helpers) + entry := fmt.Sprintf("%s:%s:65536\n", userns, hostUID) + tempDir := helpers.T().TempDir() + files := []string{"subuid", "subgid"} + for _, file := range files { + + fileBak := filepath.Join(tempDir, file) + defer os.Remove(fileBak) + d, err := os.Create(fileBak) + if err != nil { + return fmt.Errorf("failed to create %s: %w", fileBak, err) + } + + s, err := os.Open(filepath.Join("/etc", file)) + if err != nil { + return fmt.Errorf("failed to open %s: %w", file, err) + } + defer s.Close() + + _, err = io.Copy(d, s) + if err != nil { + return fmt.Errorf("failed to copy %s to %s: %w", file, fileBak, err) + } + + f, err := os.OpenFile(fmt.Sprintf("/etc/%s", file), os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open %s: %w", file, err) + } + defer f.Close() + + if _, err := f.WriteString(entry); err != nil { + return fmt.Errorf("failed to write to %s: %w", file, err) + } + } + return nil +} + +func addUser(username string, hostID string, helpers test.Helpers) { + helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{ + ExitCode: 0}) + helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{ + ExitCode: 0}) +} + +func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) { + delUser(userns, helpers) + delGroup(userns, helpers) + tempDir := helpers.T().TempDir() + files := []string{"subuid", "subgid"} + for _, file := range files { + fileBak := filepath.Join(tempDir, file) + s, err := os.Open(fileBak) + if err != nil { + t.Logf("failed to open %s, Error: %s", fileBak, err) + continue + } + defer s.Close() + + d, err := os.Open(filepath.Join("/etc/%s", file)) + if err != nil { + t.Logf("failed to open %s, Error: %s", file, err) + continue + + } + defer d.Close() + + _, err = io.Copy(d, s) + if err != nil { + t.Logf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err) + continue + } + + } +} + +func delUser(username string, helpers test.Helpers) { + helpers.Custom("userdel", username).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck}) +} + +func delGroup(groupname string, helpers test.Helpers) { + helpers.Custom("groupdel", groupname).Run(&test.Expected{ExitCode: expect.ExitCodeNoCheck}) +} diff --git a/cmd/nerdctl/container/container_create_test.go b/cmd/nerdctl/container/container_create_test.go index 8b507d37fc6..07a14a3136c 100644 --- a/cmd/nerdctl/container/container_create_test.go +++ b/cmd/nerdctl/container/container_create_test.go @@ -18,6 +18,7 @@ package container import ( "encoding/json" + "fmt" "testing" "time" @@ -35,35 +36,26 @@ 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")) + data.Labels().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, expect.Contains("Created")), - }, - { - Description: "start", - NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("start", data.Get("cID")) + return helpers.Command("ps", "-a", "--filter", "status=created", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("cID"))) }, - Expected: test.Expects(0, nil, nil), + Expected: test.Expects(0, nil, expect.Contains("Created")), }, { - Description: "logs", + Description: "start", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("logs", data.Get("cID")) + return helpers.Command("start", "-a", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Contains("foo")), }, @@ -79,7 +71,7 @@ func TestCreateHyperVContainer(t *testing.T) { 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")) + data.Labels().Set("cID", data.Identifier("container")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -98,10 +90,10 @@ func TestCreateHyperVContainer(t *testing.T) { Description: "start", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { - helpers.Ensure("start", data.Get("cID")) + helpers.Ensure("start", data.Labels().Get("cID")) ran := false for i := 0; i < 10 && !ran; i++ { - helpers.Command("container", "inspect", data.Get("cID")). + helpers.Command("container", "inspect", data.Labels().Get("cID")). Run(&test.Expected{ ExitCode: expect.ExitCodeNoCheck, Output: func(stdout string, info string, t *testing.T) { @@ -119,7 +111,7 @@ func TestCreateHyperVContainer(t *testing.T) { 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")) + return helpers.Command("logs", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, expect.Contains("foo")), }, diff --git a/cmd/nerdctl/container/container_diff_test.go b/cmd/nerdctl/container/container_diff_test.go index 07e0cb4fd45..b2ab02191ab 100644 --- a/cmd/nerdctl/container/container_diff_test.go +++ b/cmd/nerdctl/container/container_diff_test.go @@ -39,7 +39,7 @@ func TestDiff(t *testing.T) { testCase.Require = require.Not(require.Windows) testCase.Setup = func(data test.Data, helpers test.Helpers) { - helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "touch /a; touch /bin/b; rm /bin/base64") } @@ -51,12 +51,15 @@ func TestDiff(t *testing.T) { return helpers.Command("diff", data.Identifier()) } - testCase.Expected = test.Expects(0, nil, expect.All( - expect.Contains("A /a"), - expect.Contains("C /bin"), - expect.Contains("A /bin/b"), - expect.Contains("D /bin/base64"), - )) + testCase.Expected = test.Expects( + 0, + nil, + expect.Contains( + "A /a", + "C /bin", + "A /bin/b", + "D /bin/base64"), + ) testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_exec_linux_test.go b/cmd/nerdctl/container/container_exec_linux_test.go index c5624085643..9eafe7939bd 100644 --- a/cmd/nerdctl/container/container_exec_linux_test.go +++ b/cmd/nerdctl/container/container_exec_linux_test.go @@ -65,14 +65,17 @@ func TestExecTTY(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) - data.Set("container_name", data.Identifier()) + + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + data.Labels().Set("container_name", data.Identifier()) } testCase.SubTests = []*test.Case{ { Description: "stty with -it", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("exec", "-it", data.Get("container_name"), "stty") + cmd := helpers.Command("exec", "-it", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, @@ -81,7 +84,7 @@ func TestExecTTY(t *testing.T) { { Description: "stty with -t", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("exec", "-t", data.Get("container_name"), "stty") + cmd := helpers.Command("exec", "-t", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, @@ -90,7 +93,7 @@ func TestExecTTY(t *testing.T) { { Description: "stty with -i", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("exec", "-i", data.Get("container_name"), "stty") + cmd := helpers.Command("exec", "-i", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, @@ -99,7 +102,7 @@ func TestExecTTY(t *testing.T) { { Description: "stty without params", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - cmd := helpers.Command("exec", data.Get("container_name"), "stty") + cmd := helpers.Command("exec", data.Labels().Get("container_name"), "stty") cmd.WithPseudoTTY() return cmd }, diff --git a/cmd/nerdctl/container/container_inspect.go b/cmd/nerdctl/container/container_inspect.go index 8f1b6b807cf..78560b63c0e 100644 --- a/cmd/nerdctl/container/container_inspect.go +++ b/cmd/nerdctl/container/container_inspect.go @@ -21,15 +21,18 @@ import ( "github.com/spf13/cobra" + "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/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 inspectCommand() *cobra.Command { - var cmd = &cobra.Command{ + cmd := &cobra.Command{ Use: "inspect [flags] CONTAINER [CONTAINER, ...]", Short: "Display detailed information on one or more containers.", Long: "Hint: set `--mode=native` for showing the full output", @@ -100,7 +103,18 @@ func inspectAction(cmd *cobra.Command, args []string) error { } defer cancel() - return container.Inspect(ctx, client, args, opt) + entries, err := container.Inspect(ctx, client, args, opt) + if err != nil { + return err + } + + // Display + if len(entries) > 0 { + if formatErr := formatter.FormatSlice(opt.Format, opt.Stdout, entries); formatErr != nil { + log.G(ctx).Error(formatErr) + } + } + return err } func containerInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 2c7bc7b8197..7ccf35eeea9 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -19,6 +19,8 @@ package container import ( "fmt" "os" + "os/exec" + "path/filepath" "slices" "strings" "testing" @@ -26,11 +28,15 @@ import ( "github.com/docker/go-connections/nat" "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/labels" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestContainerInspectContainsPortConfig(t *testing.T) { @@ -192,7 +198,7 @@ func TestContainerInspectState(t *testing.T) { // nerdctl: run error produces a nil Task, so the Status is empty because Status comes from Task. // docker : run error gives => `Status=created` as in docker there is no a separation between container and Task. errStatus := "" - if base.Target == testutil.Docker { + if nerdtest.IsDocker() { errStatus = "created" } testCases := []testCase{ @@ -246,7 +252,6 @@ func TestContainerInspectHostConfig(t *testing.T) { base.Cmd("run", "-d", "--name", testContainer, "--cpuset-cpus", "0-1", "--cpuset-mems", "0", - "--blkio-weight", "500", "--cpu-shares", "1024", "--cpu-quota", "100000", "--group-add", "1000", @@ -258,7 +263,6 @@ func TestContainerInspectHostConfig(t *testing.T) { "--read-only", "--shm-size", "256m", "--uts", "host", - "--sysctl", "net.core.somaxconn=1024", "--runtime", "io.containerd.runc.v2", testutil.AlpineImage, "sleep", "infinity").AssertOK() @@ -266,7 +270,6 @@ func TestContainerInspectHostConfig(t *testing.T) { assert.Equal(t, "0-1", inspect.HostConfig.CPUSetCPUs) assert.Equal(t, "0", inspect.HostConfig.CPUSetMems) - assert.Equal(t, uint16(500), inspect.HostConfig.BlkioWeight) assert.Equal(t, uint64(1024), inspect.HostConfig.CPUShares) assert.Equal(t, int64(100000), inspect.HostConfig.CPUQuota) assert.Assert(t, slices.Contains(inspect.HostConfig.GroupAdd, "1000"), "Expected '1000' to be in GroupAdd") @@ -291,7 +294,7 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) { // Hostconfig default values differ with Docker. // This is because we directly retrieve the configured values instead of using preset defaults. - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { hc.Driver = "" hc.GroupAddSize = 0 hc.ShmSize = int64(67108864) // Docker default 64M @@ -311,6 +314,11 @@ func TestContainerInspectHostConfigDefaults(t *testing.T) { assert.Equal(t, "", inspect.HostConfig.CPUSetCPUs) assert.Equal(t, "", inspect.HostConfig.CPUSetMems) assert.Equal(t, uint16(0), inspect.HostConfig.BlkioWeight) + assert.Equal(t, 0, len(inspect.HostConfig.BlkioWeightDevice)) + assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadBps)) + assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceReadIOps)) + assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteBps)) + assert.Equal(t, 0, len(inspect.HostConfig.BlkioDeviceWriteIOps)) assert.Equal(t, uint64(0), inspect.HostConfig.CPUShares) assert.Equal(t, int64(0), inspect.HostConfig.CPUQuota) assert.Equal(t, hc.GroupAddSize, len(inspect.HostConfig.GroupAdd)) @@ -390,7 +398,7 @@ func TestContainerInspectHostConfigPID(t *testing.T) { var hc hostConfigValues - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { hc.PidMode = "container:" + containerID1 } else { hc.PidMode = containerID1 @@ -435,7 +443,7 @@ func TestContainerInspectDevices(t *testing.T) { t.Fatal(err) } - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { dir = "/dev/zero" } @@ -456,6 +464,95 @@ func TestContainerInspectDevices(t *testing.T) { assert.DeepEqual(t, expectedDevices, inspect.HostConfig.Devices) } +func TestContainerInspectBlkioSettings(t *testing.T) { + testutil.DockerIncompatible(t) + testContainer := testutil.Identifier(t) + // Some of the blkio settings are not supported in cgroup v1. + // So skip this test if running on cgroup v1 + if infoutil.CgroupsVersion() == "1" { + t.Skip("test skipped for rootless containers or if running with cgroup v1") + } + + if rootlessutil.IsRootless() { + t.Skip("test requires root privilege to create a dummy device") + } + + // See https://github.com/containerd/nerdctl/issues/4185 + // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. + // For now, disable the test unless on a recent kernel. + testutil.RequireKernelVersion(t, ">= 6.0.0-0") + + devPath := "/dev/dummy-zero" + // a dummy zero device: mknod /dev/dummy-zero c 1 5 + helperCmd := exec.Command("mknod", []string{devPath, "c", "1", "5"}...) + if out, err := helperCmd.CombinedOutput(); err != nil { + err = fmt.Errorf("cannot create %q: %q: %w", devPath, string(out), err) + t.Fatal(err) + } + + // ensure the file will be removed in case of failed in the test + defer func() { + if err := exec.Command("rm", "-f", devPath).Run(); err != nil { + t.Logf("failed to remove device %s: %v", devPath, err) + } + }() + + base := testutil.NewBase(t) + defer base.Cmd("rm", "-f", testContainer).AssertOK() + + base.Cmd("run", "-d", "--name", testContainer, + "--blkio-weight", "500", + "--blkio-weight-device", "/dev/dummy-zero:500", + "--device-read-bps", "/dev/dummy-zero:1048576", + "--device-read-iops", "/dev/dummy-zero:1000", + "--device-write-bps", "/dev/dummy-zero:2097152", + "--device-write-iops", "/dev/dummy-zero:2000", + testutil.AlpineImage, "sleep", "infinity").AssertOK() + + inspect := base.InspectContainer(testContainer) + assert.Equal(t, uint16(500), inspect.HostConfig.BlkioWeight) + assert.Equal(t, 1, len(inspect.HostConfig.BlkioWeightDevice)) + assert.Equal(t, uint16(500), *inspect.HostConfig.BlkioWeightDevice[0].Weight) + assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadBps)) + assert.Equal(t, uint64(1048576), inspect.HostConfig.BlkioDeviceReadBps[0].Rate) + assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteBps)) + assert.Equal(t, uint64(2097152), inspect.HostConfig.BlkioDeviceWriteBps[0].Rate) + assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceReadIOps)) + assert.Equal(t, uint64(1000), inspect.HostConfig.BlkioDeviceReadIOps[0].Rate) + assert.Equal(t, 1, len(inspect.HostConfig.BlkioDeviceWriteIOps)) + assert.Equal(t, uint64(2000), inspect.HostConfig.BlkioDeviceWriteIOps[0].Rate) +} + +func TestContainerInspectUser(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Description: "Container inspect contains User", + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf(` +FROM %s +RUN groupadd -r test && useradd -r -g test test +USER test +`, testutil.UbuntuImage) + + err := os.WriteFile(filepath.Join(data.Temp().Path(), "Dockerfile"), []byte(dockerfile), 0o600) + assert.NilError(helpers.T(), err) + + helpers.Ensure("build", "-t", data.Identifier(), data.Temp().Path()) + helpers.Ensure("create", "--name", data.Identifier(), "--user", "test", data.Identifier()) + }, + 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("inspect", "--format", "{{.Config.User}}", data.Identifier()) + }, + Expected: test.Expects(0, nil, expect.Equals("test\n")), + } + + testCase.Run(t) +} + type hostConfigValues struct { Driver string ShmSize int64 diff --git a/cmd/nerdctl/container/container_kill_linux_test.go b/cmd/nerdctl/container/container_kill_linux_test.go index 0e7d3fc3f62..6372d80ee33 100644 --- a/cmd/nerdctl/container/container_kill_linux_test.go +++ b/cmd/nerdctl/container/container_kill_linux_test.go @@ -27,6 +27,7 @@ import ( "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/nerdtest" ) // TestKillCleanupForwards runs a container that exposes a port and then kill it. @@ -66,7 +67,7 @@ func TestKillCleanupForwards(t *testing.T) { // define iptables chain name depending on the target (docker/nerdctl) var chain string - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { chain = "DOCKER" } else { redirectChain := "CNI-HOSTPORT-DNAT" diff --git a/cmd/nerdctl/container/container_list_linux_test.go b/cmd/nerdctl/container/container_list_linux_test.go index 9e3e1922a03..e7ce1c92e11 100644 --- a/cmd/nerdctl/container/container_list_linux_test.go +++ b/cmd/nerdctl/container/container_list_linux_test.go @@ -26,10 +26,13 @@ import ( "gotest.tools/v3/assert" + "github.com/containerd/nerdctl/mod/tigron/test" + "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" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) type psTestContainer struct { @@ -146,7 +149,7 @@ func TestContainerList(t *testing.T) { // there is some difference between nerdctl and docker in calculating the size of the container expectedSize := "26.2MB (virtual " - if base.Target != testutil.Docker { + if !nerdtest.IsDocker() { expectedSize = "25.0 MiB (virtual " } @@ -627,3 +630,35 @@ func TestContainerListCheckCreatedTime(t *testing.T) { t.Errorf("expected containers in decending order") } } + +func TestContainerListStatusFilter(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.Labels().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{ + // TODO: Refactor other filter tests + { + Description: "ps filter with status=created", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("ps", "-a", "--filter", "status=created", "--filter", fmt.Sprintf("name=%s", data.Labels().Get("cID"))) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout, info string, t *testing.T) { + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("cID")), "No container found with status created") + }, + } + }, + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_logs.go b/cmd/nerdctl/container/container_logs.go index 4c8a3095bb9..0543e97e0eb 100644 --- a/cmd/nerdctl/container/container_logs.go +++ b/cmd/nerdctl/container/container_logs.go @@ -53,6 +53,7 @@ The following containers are supported: cmd.Flags().StringP("tail", "n", "all", "Number of lines to show from the end of the logs") cmd.Flags().String("since", "", "Show logs since timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") cmd.Flags().String("until", "", "Show logs before a timestamp (e.g. 2013-01-02T13:23:37Z) or relative (e.g. 42m for 42 minutes)") + cmd.Flags().Bool("details", false, "Show extra details provided to logs") return cmd } @@ -88,6 +89,10 @@ func logsOptions(cmd *cobra.Command) (types.ContainerLogsOptions, error) { if err != nil { return types.ContainerLogsOptions{}, err } + details, err := cmd.Flags().GetBool("details") + if err != nil { + return types.ContainerLogsOptions{}, err + } return types.ContainerLogsOptions{ Stdout: cmd.OutOrStdout(), Stderr: cmd.OutOrStderr(), @@ -97,6 +102,7 @@ func logsOptions(cmd *cobra.Command) (types.ContainerLogsOptions, error) { Tail: tail, Since: since, Until: until, + Details: details, }, nil } diff --git a/cmd/nerdctl/container/container_logs_test.go b/cmd/nerdctl/container/container_logs_test.go index 94c561fafb4..632ce955949 100644 --- a/cmd/nerdctl/container/container_logs_test.go +++ b/cmd/nerdctl/container/container_logs_test.go @@ -17,15 +17,16 @@ package container import ( + "errors" "fmt" - "os/exec" + "regexp" "runtime" + "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" - "gotest.tools/v3/icmd" "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" @@ -36,181 +37,324 @@ import ( ) func TestLogs(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) const expected = `foo -bar` +bar +` - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - - //test since / until flag - time.Sleep(3 * time.Second) - base.Cmd("logs", "--since", "1s", containerName).AssertOutNotContains(expected) - base.Cmd("logs", "--since", "10s", containerName).AssertOutContains(expected) - base.Cmd("logs", "--until", "10s", containerName).AssertOutNotContains(expected) - base.Cmd("logs", "--until", "1s", containerName).AssertOutContains(expected) + testCase := nerdtest.Setup() - // Ensure follow flag works as expected: - base.Cmd("logs", "-f", containerName).AssertOutContains("bar") - base.Cmd("logs", "-f", containerName).AssertOutContains("foo") + if runtime.GOOS == "windows" { + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } - //test timestamps flag - base.Cmd("logs", "-t", containerName).AssertOutContains(time.Now().UTC().Format("2006-01-02")) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - //test tail flag - base.Cmd("logs", "-n", "all", containerName).AssertOutContains(expected) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--quiet", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar;") + data.Labels().Set("cID", data.Identifier()) + } - base.Cmd("logs", "-n", "1", containerName).AssertOutWithFunc(func(stdout string) error { - if !(stdout == "bar\n" || stdout == "") { - return fmt.Errorf("expected %q or %q, got %q", "bar", "", stdout) - } - return nil - }) + testCase.SubTests = []*test.Case{ + { + Description: "since 1s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "1s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), + }, + { + Description: "since 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "until 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--until", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.DoesNotContain(expected)), + }, + { + Description: "until 1s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--until", "1s", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "follow", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "timestamp", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-t", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Contains(time.Now().UTC().Format("2006-01-02"))), + }, + { + Description: "tail flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-n", "all", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals(expected)), + }, + { + Description: "tail flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-n", "1", data.Labels().Get("cID")) + }, + // FIXME: why? + Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))), + }, + } - base.Cmd("rm", "-f", containerName).AssertOK() + testCase.Run(t) } // Tests whether `nerdctl logs` properly separates stdout/stderr output // streams for containers using the jsonfile logging driver: func TestLogsOutStreamsSeparated(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + testCase := nerdtest.Setup() + + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euc", "echo stdout1; echo stderr1 >&2; echo stdout2; echo stderr2 >&2") + } + + 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("logs", data.Identifier()) + } - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euc", "echo stdout1; echo stderr1 >&2; echo stdout2; echo stderr2 >&2").AssertOK() - time.Sleep(3 * time.Second) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, []error{ + //revive:disable:error-strings + errors.New("stderr1\nstderr2\n"), + }, expect.Equals("stdout1\nstdout2\n")) - base.Cmd("logs", containerName).AssertOutStreamsExactly("stdout1\nstdout2\n", "stderr1\nstderr2\n") + testCase.Run(t) } func TestLogsWithInheritedFlags(t *testing.T) { - // Seen flaky with Docker - t.Parallel() - base := testutil.NewBase(t) - for k, v := range base.Args { - if strings.HasPrefix(v, "--namespace=") { - base.Args[k] = "-n=" + testutil.Namespace - } + testCase := nerdtest.Setup() + + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("-n="+testutil.Namespace, "run", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") } - containerName := testutil.Identifier(t) - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - // 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 == "") { - return fmt.Errorf("expected %q or %q, got %q", "bar", "", stdout) - } - return nil - }) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("-n="+testutil.Namespace, "logs", "-n", "1", data.Identifier()) + } + + // FIXME: why? + testCase.Expected = test.Expects(0, nil, expect.Match(regexp.MustCompile("^(?:bar\n|)$"))) + + testCase.Run(t) } func TestLogsOfJournaldDriver(t *testing.T) { - testutil.RequireExecutable(t, "journalctl") - journalctl, _ := exec.LookPath("journalctl") - res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) - if res.ExitCode != 0 { - t.Skipf("current user is not allowed to access journal logs: %s", res.Combined()) - } + const expected = `foo +bar +` - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + testCase := nerdtest.Setup() - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--network", "none", "--log-driver", "journald", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() + testCase.Require = require.All( + require.Binary("journalctl"), + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + works := false + cmd := helpers.Custom("journalctl", "-xe") + cmd.Run(&test.Expected{ + ExitCode: expect.ExitCodeNoCheck, + Output: func(stdout, info string, t *testing.T) { + if stdout != "" { + works = true + } + }, + }) + return works, "Journactl to return data for the current user" + }, + }, + ) - time.Sleep(3 * time.Second) - base.Cmd("logs", containerName).AssertOutContains("bar") - // Run logs twice, make sure that the logs are not removed - base.Cmd("logs", containerName).AssertOutContains("foo") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - base.Cmd("logs", "--since", "5s", containerName).AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "bar") { - return fmt.Errorf("expected bar, got %s", stdout) - } - if !strings.Contains(stdout, "foo") { - return fmt.Errorf("expected foo, got %s", stdout) - } - return nil - }) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--network", "none", "--log-driver", "journald", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + data.Labels().Set("cID", data.Identifier()) + } - base.Cmd("rm", "-f", containerName).AssertOK() + testCase.SubTests = []*test.Case{ + { + Description: "logs", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Labels().Get("cID")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(expected)), + }, + { + Description: "logs --since 60s", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "--since", "60s", data.Labels().Get("cID")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.DoesNotContain("foo", "bar")), + }, + } } func TestLogsWithFailingContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar; exit 42; echo baz").AssertOK() - time.Sleep(3 * time.Second) - // 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).AssertOutNotContains("baz") - base.Cmd("rm", "-f", containerName).AssertOK() + const expected = `foo +bar +` + + testCase := nerdtest.Setup() + + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euxc", "echo foo; echo bar; exit 42; echo baz") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Equals(expected)) + + testCase.Run(t) } func TestLogsWithRunningContainer(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).Run() expected := make([]string, 10) for i := 0; i < 10; i++ { expected[i] = fmt.Sprint(i + 1) } - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done").AssertOK() - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) + testCase := nerdtest.Setup() + + if runtime.GOOS == "windows" { + // Logging seems broken on windows. + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "sh", "-euc", "for i in `seq 1 10`; do echo $i; sleep 1; done") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains(expected[0], expected[1:]...)) + + testCase.Run(t) } func TestLogsWithoutNewlineOrEOF(t *testing.T) { - if runtime.GOOS != "linux" { - t.Skip("FIXME: test does not work on Windows yet because containerd doesn't send an exit event appropriately after task exit on Windows") - } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).Run() - expected := []string{"Hello World!", "There is no newline"} - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "printf", "'Hello World!\nThere is no newline'").AssertOK() - time.Sleep(3 * time.Second) - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) + testCase := nerdtest.Setup() + + // FIXME: test does not work on Windows yet because containerd doesn't send an exit event appropriately after task exit on Windows") + // FIXME: nerdctl behavior does not match docker - test disabled for nerdctl until we fix + testCase.Require = require.All( + require.Linux, + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, "printf", "'Hello World!\nThere is no newline'") + } + + 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("logs", "-f", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")) + + testCase.Run(t) } func TestLogsAfterRestartingContainer(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("FIXME: test does not work on Windows yet. Restarting a container fails with: failed to create shim task: hcs::CreateComputeSystem : The requested operation for attach namespace failed.: unknown") } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - defer base.Cmd("rm", "-f", containerName).Run() - base.Cmd("run", "-d", "--name", containerName, testutil.CommonImage, - "printf", "'Hello World!\nThere is no newline'").AssertOK() - expected := []string{"Hello World!", "There is no newline"} - time.Sleep(3 * time.Second) - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) - // restart and check logs again - base.Cmd("start", containerName) - time.Sleep(3 * time.Second) - base.Cmd("logs", "-f", containerName).AssertOutContainsAll(expected...) + + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, + "printf", "'Hello World!\nThere is no newline'") + data.Labels().Set("cID", data.Identifier()) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.SubTests = []*test.Case{ + { + Description: "logs -f works", + NoParallel: true, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline'")), + }, + { + Description: "logs -f works after restart", + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("start", data.Labels().Get("cID")) + // FIXME: this is inherently flaky + time.Sleep(5 * time.Second) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", "-f", data.Labels().Get("cID")) + }, + Expected: test.Expects(0, nil, expect.Equals("'Hello World!\nThere is no newline''Hello World!\nThere is no newline'")), + }, + } + + testCase.Run(t) } func TestLogsWithForegroundContainers(t *testing.T) { @@ -232,11 +376,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo"), - expect.Contains("bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactive", @@ -249,11 +389,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo"), - expect.Contains("bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "PTY", @@ -268,11 +404,7 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo"), - expect.Contains("bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, { Description: "interactivePTY", @@ -287,61 +419,198 @@ func TestLogsWithForegroundContainers(t *testing.T) { Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("logs", data.Identifier()) }, - Expected: test.Expects(0, nil, expect.All( - expect.Contains("foo"), - expect.Contains("bar"), - expect.DoesNotContain("baz"), - )), + Expected: test.Expects(0, nil, expect.Equals("foo\nbar\n")), }, } } -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) - +func TestLogsTailFollowRotate(t *testing.T) { + // FIXME this is flaky by nature... the number of lines is arbitrary, the wait is arbitrary, + // and both are some sort of educated guess that things will mostly always kinda work maybe... 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) - } + testCase := nerdtest.Setup() + + // tail log is not supported on Windows + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--log-driver", "json-file", + "--log-opt", fmt.Sprintf("max-size=%d", len(sampleJSONLog)*linesPerFile), + "--log-opt", "max-file=10", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euc", "while true; do echo A; usleep 100; done") + // FIXME: ... inherently racy... + time.Sleep(5 * time.Second) + } + + 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 { + cmd := helpers.Command("logs", "-f", data.Identifier()) + // FIXME: this is flaky by nature. We assume that the container has started and will output enough in 5 seconds. + cmd.WithTimeout(5 * time.Second) + return cmd } - assert.Equal(t, true, len(tailLogs) > linesPerFile, logRun.Stderr()) + + testCase.Expected = test.Expects(expect.ExitCodeTimeout, nil, func(stdout, info string, t *testing.T) { + tailLogs := strings.Split(strings.TrimSpace(stdout), "\n") + for _, line := range tailLogs { + if line != "" { + assert.Equal(t, "A", line) + } + } + + assert.Assert(t, len(tailLogs) > linesPerFile, fmt.Sprintf("expected %d lines or more, found %d", linesPerFile, len(tailLogs))) + }) + + testCase.Run(t) } -func TestNoneLoggerHasNoLogURI(t *testing.T) { + +func TestLogsNoneLoggerHasNoLogURI(t *testing.T) { testCase := nerdtest.Setup() testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("run", "--name", data.Identifier(), "--log-driver", "none", testutil.CommonImage, "sh", "-euxc", "echo foo") } + 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("logs", data.Identifier()) } + testCase.Expected = test.Expects(1, nil, nil) + + testCase.Run(t) +} + +func TestLogsWithDetails(t *testing.T) { + testCase := nerdtest.Setup() + + // FIXME: this is not working on windows. There is some deep issue with windows logs: + // https://github.com/containerd/nerdctl/issues/4237 + if runtime.GOOS == "windows" { + testCase.Require = nerdtest.NerdctlNeedsFixing("https://github.com/containerd/nerdctl/issues/4237") + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--log-driver", "json-file", + "--log-opt", "max-size=10m", + "--log-opt", "max-file=3", + "--log-opt", "env=ENV", + "--env", "ENV=foo", + "--log-opt", "labels=LABEL", + "--label", "LABEL=bar", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-ec", "echo baz") + } + + 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("logs", "--details", data.Identifier()) + } + + testCase.Expected = test.Expects(0, nil, expect.Contains("ENV=foo", "LABEL=bar", "baz")) + + testCase.Run(t) +} + +func TestLogsFollowNoExtraneousLineFeed(t *testing.T) { + testCase := nerdtest.Setup() + // This test verifies that `nerdctl logs -f` does not add extraneous line feeds + testCase.Require = require.Not(require.Windows) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Create a container that outputs a message without a trailing newline + helpers.Ensure("run", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-c", "printf 'Hello without newline'") + } + + 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 { + // Use logs -f to follow the logs + return helpers.Command("logs", "-f", data.Identifier()) + } + + // Verify that the output is exactly "Hello without newline" without any additional line feeds + testCase.Expected = test.Expects(0, nil, expect.Equals("Hello without newline")) + + testCase.Run(t) +} + +func TestLogsWithStartContainer(t *testing.T) { + testCase := nerdtest.Setup() + + // Windows does not support dual logging. + testCase.Require = require.Not(require.Windows) + + testCase.SubTests = []*test.Case{ + { + Description: "Test logs are directed correctly for container start of a interactive container", + Setup: func(data test.Data, helpers test.Helpers) { + cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo foo\nexit\n")) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + + cmd = helpers.Command("start", "-ia", data.Identifier()) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo bar\nexit\n")) + cmd.Run(&test.Expected{ + ExitCode: 0, + }) + }, + 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("logs", data.Identifier()) + }, + Expected: test.Expects(0, nil, expect.Contains("foo", "bar")), + }, + { + // FIXME: is this test safe or could it be racy? + Description: "Test logs are captured after stopping and starting a non-interactive container and continue capturing new logs", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.CommonImage, "sh", "-c", "while true; do echo foo; sleep 1; done") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Ensure("stop", data.Identifier()) + initialLogs := helpers.Capture("logs", data.Identifier()) + initialFooCount := strings.Count(initialLogs, "foo") + data.Labels().Set("initialFooCount", strconv.Itoa(initialFooCount)) + helpers.Ensure("start", data.Identifier()) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + return helpers.Command("logs", 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) { + finalLogsCount := strings.Count(stdout, "foo") + initialFooCount, _ := strconv.Atoi(data.Labels().Get("initialFooCount")) + assert.Assert(t, finalLogsCount > initialFooCount, "Expected 'foo' count to increase after restart", info) + }, + } + }, + }, + } testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_restart_linux_test.go b/cmd/nerdctl/container/container_restart_linux_test.go index 95b404099cd..f4d82482f1f 100644 --- a/cmd/nerdctl/container/container_restart_linux_test.go +++ b/cmd/nerdctl/container/container_restart_linux_test.go @@ -26,6 +26,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -129,6 +130,9 @@ func TestRestartWithTime(t *testing.T) { func TestRestartWithSignal(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: gomodjail signal handling is not working yet: https://github.com/AkihiroSuda/gomodjail/issues/51 + testCase.Require = require.Not(nerdtest.Gomodjail) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) } @@ -136,7 +140,7 @@ func TestRestartWithSignal(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := nerdtest.RunSigProxyContainer(nerdtest.SigUsr1, false, nil, data, helpers) // Capture the current pid - data.Set("oldpid", strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid)) + data.Labels().Set("oldpid", strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid)) // Send the signal helpers.Ensure("restart", "--signal", "SIGUSR1", data.Identifier()) return cmd @@ -154,7 +158,7 @@ func TestRestartWithSignal(t *testing.T) { nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // Check the new pid is different newpid := strconv.Itoa(nerdtest.InspectContainer(helpers, data.Identifier()).State.Pid) - assert.Assert(helpers.T(), newpid != data.Get("oldpid"), info) + assert.Assert(helpers.T(), newpid != data.Labels().Get("oldpid"), info) }, ), } diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index e45a1617c5a..be629b7eb2f 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/spf13/cobra" + "golang.org/x/term" "github.com/containerd/console" "github.com/containerd/log" @@ -155,7 +156,6 @@ func setCreateFlags(cmd *cobra.Command) { }) cmd.Flags().Int64("pids-limit", -1, "Tune container pids limit (set -1 for unlimited)") cmd.Flags().StringSlice("cgroup-conf", nil, "Configure cgroup v2 (key=value)") - cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") cmd.Flags().String("cgroupns", defaults.CgroupnsMode(), `Cgroup namespace to use, the default depends on the cgroup version ("host"|"private")`) cmd.Flags().String("cgroup-parent", "", "Optional parent cgroup for the container") cmd.RegisterFlagCompletionFunc("cgroupns", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -166,6 +166,8 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().Uint64("cpu-shares", 0, "CPU shares (relative weight)") cmd.Flags().Int64("cpu-quota", -1, "Limit CPU CFS (Completely Fair Scheduler) quota") cmd.Flags().Uint64("cpu-period", 0, "Limit CPU CFS (Completely Fair Scheduler) period") + cmd.Flags().Uint64("cpu-rt-period", 0, "Limit CPU real-time period in microseconds") + cmd.Flags().Uint64("cpu-rt-runtime", 0, "Limit CPU real-time runtime in microseconds") // device is defined as StringSlice, not StringArray, to allow specifying "--device=DEV1,DEV2" (compatible with Podman) cmd.Flags().StringSlice("device", nil, "Add a host device to the container") // ulimit is defined as StringSlice, not StringArray, to allow specifying "--ulimit=ULIMIT1,ULIMIT2" (compatible with Podman) @@ -173,6 +175,15 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().String("rdt-class", "", "Name of the RDT class (or CLOS) to associate the container with") // #endregion + // #region blkio flags + cmd.Flags().Uint16("blkio-weight", 0, "Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)") + cmd.Flags().StringArray("blkio-weight-device", []string{}, "Block IO weight (relative device weight) (default [])") + cmd.Flags().StringArray("device-read-bps", []string{}, "Limit read rate (bytes per second) from a device (default [])") + cmd.Flags().StringArray("device-read-iops", []string{}, "Limit read rate (IO per second) from a device (default [])") + cmd.Flags().StringArray("device-write-bps", []string{}, "Limit write rate (bytes per second) to a device (default [])") + cmd.Flags().StringArray("device-write-iops", []string{}, "Limit write rate (IO per second) to a device (default [])") + // #endregion + // user flags cmd.Flags().StringP("user", "u", "", "Username or UID (format: [:])") cmd.Flags().String("umask", "", "Set the umask inside the container. Defaults to 0022") @@ -277,7 +288,6 @@ func setCreateFlags(cmd *cobra.Command) { // #endregion cmd.Flags().String("ipfs-address", "", "multiaddr of IPFS API (default uses $IPFS_PATH env variable if defined or local directory ~/.ipfs)") - cmd.Flags().String("isolation", "default", "Specify isolation technology for container. On Linux the only valid value is default. Windows options are host, process and hyperv with process isolation as the default") cmd.RegisterFlagCompletionFunc("isolation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if runtime.GOOS == "windows" { @@ -285,6 +295,7 @@ func setCreateFlags(cmd *cobra.Command) { } return []string{"default"}, cobra.ShellCompDirectiveNoFileComp }) + cmd.Flags().String("userns", "", "Specify host to disable userns-remap") } @@ -399,7 +410,7 @@ func runAction(cmd *cobra.Command, args []string) error { return err } defer con.Reset() - if err := con.SetRaw(); err != nil { + if _, err := term.MakeRaw(int(con.Fd())); err != nil { return err } } diff --git a/cmd/nerdctl/container/container_run_cgroup_linux_test.go b/cmd/nerdctl/container/container_run_cgroup_linux_test.go index edf42711730..dd6e8f93fdf 100644 --- a/cmd/nerdctl/container/container_run_cgroup_linux_test.go +++ b/cmd/nerdctl/container/container_run_cgroup_linux_test.go @@ -21,8 +21,10 @@ import ( "context" "fmt" "os" + "os/exec" "path/filepath" "strconv" + "strings" "testing" "gotest.tools/v3/assert" @@ -108,7 +110,7 @@ func TestRunCgroupV2(t *testing.T) { update := []string{"update", "--cpu-quota", "42000", "--cpuset-mems", "0", "--cpu-period", "100000", "--memory", "42m", "--pids-limit", "42", "--cpu-shares", "2000", "--cpuset-cpus", "0-1"} - if base.Target == testutil.Docker && info.CgroupVersion == "2" && info.SwapLimit { + if nerdtest.IsDocker() && info.CgroupVersion == "2" && info.SwapLimit { // Workaround for Docker with cgroup v2: // > Error response from daemon: Cannot update container 67c13276a13dd6a091cdfdebb355aa4e1ecb15fbf39c2b5c9abee89053e88fce: // > Memory limit should be smaller than already set memoryswap limit, update the memoryswap at the same time @@ -132,7 +134,9 @@ func TestRunCgroupV2(t *testing.T) { base.Cmd("exec", testutil.Identifier(t)+"-testUpdate2", "cat", "cpu.max", "memory.max", "memory.swap.max", "memory.low", "pids.max", "cpu.weight", "cpuset.cpus", "cpuset.mems").AssertOutExactly(expected2) - + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertOK() + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() + base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/foo").AssertFail() } func TestRunCgroupV1(t *testing.T) { @@ -174,6 +178,9 @@ func TestRunCgroupV1(t *testing.T) { const expected = "42000\n100000\n0\n44040192\n6291456\n104857600\n0\n42\n2000\n0-1\n" base.Cmd("run", "--rm", "--cpus", "0.42", "--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) 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) + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=true", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertOK() + base.Cmd("run", "--rm", "--security-opt", "writable-cgroups=false", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() + base.Cmd("run", "--rm", testutil.AlpineImage, "mkdir", "/sys/fs/cgroup/pids/foo").AssertFail() } // TestIssue3781 tests https://github.com/containerd/nerdctl/issues/3781 @@ -240,7 +247,7 @@ func TestRunDevice(t *testing.T) { t.Logf("lo[%d] = %+v", i, lo[i]) loContent := fmt.Sprintf("lo%d-content", i) assert.NilError(t, os.WriteFile(lo[i].Device, []byte(loContent), 0o700)) - data.Set("loContent"+strconv.Itoa(i), loContent) + data.Labels().Set("loContent"+strconv.Itoa(i), loContent) } // lo0 is readable but not writable. @@ -252,7 +259,7 @@ func TestRunDevice(t *testing.T) { "--device", lo[0].Device+":r", "--device", lo[1].Device, testutil.AlpineImage, "sleep", nerdtest.Infinity) - data.Set("id", data.Identifier()) + data.Labels().Set("id", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -268,25 +275,25 @@ func TestRunDevice(t *testing.T) { { Description: "can read lo0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("exec", data.Get("id"), "cat", lo[0].Device) + return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[0].Device) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("locontent0")), + Output: expect.Contains(data.Labels().Get("locontent0")), } }, }, { Description: "cannot write lo0", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("exec", data.Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[0].Device) + return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[0].Device) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "cannot read lo2", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("exec", data.Get("id"), "cat", lo[2].Device) + return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[2].Device) }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, @@ -294,11 +301,11 @@ func TestRunDevice(t *testing.T) { Description: "can read lo1", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("exec", data.Get("id"), "cat", lo[1].Device) + return helpers.Command("exec", data.Labels().Get("id"), "cat", lo[1].Device) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("locontent1")), + Output: expect.Contains(data.Labels().Get("locontent1")), } }, }, @@ -306,7 +313,7 @@ func TestRunDevice(t *testing.T) { Description: "can write lo1 and read back updated value", NoParallel: true, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("exec", data.Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device) + return helpers.Command("exec", data.Labels().Get("id"), "sh", "-ec", "echo -n \"overwritten-lo1-content\">"+lo[1].Device) }, Expected: test.Expects(expect.ExitCodeSuccess, nil, func(stdout string, info string, t *testing.T) { lo1Read, err := os.ReadFile(lo[1].Device) @@ -448,7 +455,7 @@ 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 { + if nerdtest.IsDocker() { expected = filepath.Join(parent, fmt.Sprintf("docker-%s", id)) } } @@ -477,3 +484,233 @@ func TestRunBlkioWeightCgroupV2(t *testing.T) { base.Cmd("update", containerName, "--blkio-weight", "400").AssertOK() base.Cmd("exec", containerName, "cat", "io.bfq.weight").AssertOutExactly("default 400\n") } + +func TestRunBlkioSettingCgroupV2(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = nerdtest.Rootful + + // See https://github.com/containerd/nerdctl/issues/4185 + // It is unclear if this is truly a kernel version problem, a runc issue, or a distro (EL9) issue. + // For now, disable the test unless on a recent kernel. + testutil.RequireKernelVersion(t, ">= 6.0.0-0") + + // Create dummy device path + dummyDev := "/dev/dummy-zero" + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Create dummy device + helperCmd := exec.Command("mknod", dummyDev, "c", "1", "5") + if out, err := helperCmd.CombinedOutput(); err != nil { + t.Fatalf("cannot create %q: %q: %v", dummyDev, string(out), err) + } + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + // Clean up the dummy device + if err := exec.Command("rm", "-f", dummyDev).Run(); err != nil { + t.Logf("failed to remove device %s: %v", dummyDev, err) + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "blkio-weight", + Require: nerdtest.CGroupV2, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--blkio-weight", "150", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + assert.Assert(t, strings.Contains(helpers.Capture("inspect", "--format", "{{.HostConfig.BlkioWeight}}", data.Identifier()), "150")) + }, + ), + } + }, + }, + { + Description: "blkio-weight-device", + Require: nerdtest.CGroupV2, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--blkio-weight-device", dummyDev+":100", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioWeightDevice}}{{.Weight}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, "100")) + }, + ), + } + }, + }, + { + Description: "device-read-bps", + Require: require.All( + nerdtest.CGroupV2, + // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options + // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases + // but not currently available in the v26 release. + require.Not(nerdtest.Docker), + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--device-read-bps", dummyDev+":1048576", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadBps}}{{.Rate}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, "1048576")) + }, + ), + } + }, + }, + { + Description: "device-write-bps", + Require: require.All( + nerdtest.CGroupV2, + // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options + // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases + // but not currently available in the v26 release. + require.Not(nerdtest.Docker), + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--device-write-bps", dummyDev+":2097152", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteBps}}{{.Rate}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, "2097152")) + }, + ), + } + }, + }, + { + Description: "device-read-iops", + Require: require.All( + nerdtest.CGroupV2, + // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options + // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases + // but not currently available in the v26 release. + require.Not(nerdtest.Docker), + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--device-read-iops", dummyDev+":1000", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceReadIOps}}{{.Rate}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, "1000")) + }, + ), + } + }, + }, + { + Description: "device-write-iops", + Require: require.All( + nerdtest.CGroupV2, + // Docker cli (v26.1.3) available in github runners has a bug where some of the blkio options + // do not work https://github.com/docker/cli/issues/5321. The fix has been merged to the latest releases + // but not currently available in the v26 release. + require.Not(nerdtest.Docker), + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--device-write-iops", dummyDev+":2000", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + inspectOut := helpers.Capture("inspect", "--format", "{{range .HostConfig.BlkioDeviceWriteIOps}}{{.Rate}}{{end}}", data.Identifier()) + assert.Assert(t, strings.Contains(inspectOut, "2000")) + }, + ), + } + }, + }, + } + + testCase.Run(t) +} + +func TestRunCPURealTimeSettingCgroupV1(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Description: "cpu-rt-runtime-and-period", + Require: require.All( + require.Not(nerdtest.CGroupV2), + nerdtest.Rootful, + ), + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", "--name", data.Identifier(), + "--cpu-rt-runtime", "950000", + "--cpu-rt-period", "1000000", + testutil.AlpineImage, "sleep", "infinity") + }, + 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, + Output: expect.All( + func(stdout string, info string, t *testing.T) { + rtRuntime := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimeRuntime}}", data.Identifier()) + rtPeriod := helpers.Capture("inspect", "--format", "{{.HostConfig.CPURealtimePeriod}}", data.Identifier()) + assert.Assert(t, strings.Contains(rtRuntime, "950000")) + assert.Assert(t, strings.Contains(rtPeriod, "1000000")) + }, + ), + } + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run_linux_test.go b/cmd/nerdctl/container/container_run_linux_test.go index efffd92196f..b025f681cfa 100644 --- a/cmd/nerdctl/container/container_run_linux_test.go +++ b/cmd/nerdctl/container/container_run_linux_test.go @@ -34,6 +34,7 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" @@ -364,12 +365,27 @@ func TestRunTTY(t *testing.T) { }, Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, + { + Description: "stty with -td", + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "-td", data.Identifier(), "stty") + cmd.WithPseudoTTY() + return cmd + }, + Expected: test.Expects(0, nil, nil), + }, } } func TestRunSigProxy(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: gomodjail signal handling is not working yet: https://github.com/AkihiroSuda/gomodjail/issues/51 + testCase.Require = require.Not(nerdtest.Gomodjail) + testCase.SubTests = []*test.Case{ { Description: "SigProxyDefault", @@ -379,6 +395,7 @@ func TestRunSigProxy(t *testing.T) { }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // FIXME: os.Interrupt will likely not work on Windows cmd := nerdtest.RunSigProxyContainer(os.Interrupt, true, nil, data, helpers) err := cmd.Signal(os.Interrupt) assert.NilError(helpers.T(), err) @@ -406,6 +423,10 @@ func TestRunSigProxy(t *testing.T) { { Description: "SigProxyFalse", + // Docker behavior changed sometimes with Docker 27 + // See https://github.com/containerd/nerdctl/issues/4219 for details + Require: require.Not(nerdtest.Docker), + Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, @@ -417,7 +438,7 @@ func TestRunSigProxy(t *testing.T) { return cmd }, - Expected: test.Expects(127, nil, expect.DoesNotContain(nerdtest.SignalCaught)), + Expected: test.Expects(expect.ExitCodeSignaled, nil, expect.DoesNotContain(nerdtest.SignalCaught)), }, } @@ -503,8 +524,9 @@ func TestRunWithDetachKeys(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("run", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) - cmd.WithPseudoTTY(func(f *os.File) error { - _, _ = f.WriteString("echo mark${NON}mark\n") + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) + cmd.WithFeeder(func() io.Reader { // Because of the way we proxy stdin, we have to wait here, otherwise we detach before // the rest of the input ever reaches the container // Note that this only concerns nerdctl, as docker seems to behave ok LOCALLY. @@ -514,8 +536,7 @@ func TestRunWithDetachKeys(t *testing.T) { nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // } // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{1, 2}) - return err + return bytes.NewReader([]byte{1, 2}) }) return cmd @@ -571,8 +592,9 @@ func TestIssue3568(t *testing.T) { testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { // Run interactively and detach cmd := helpers.Command("run", "--rm", "-it", "--detach-keys=ctrl-a,ctrl-b", "--name", data.Identifier(), testutil.CommonImage) - cmd.WithPseudoTTY(func(f *os.File) error { - _, _ = f.WriteString("echo mark${NON}mark\n") + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("echo mark${NON}mark\n")) + cmd.WithFeeder(func() io.Reader { // Because of the way we proxy stdin, we have to wait here, otherwise we detach before // the rest of the input ever reaches the container // Note that this only concerns nerdctl, as docker seems to behave ok LOCALLY. @@ -582,8 +604,7 @@ func TestIssue3568(t *testing.T) { nerdtest.EnsureContainerStarted(helpers, data.Identifier()) // } // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{1, 2}) - return err + return bytes.NewReader([]byte{1, 2}) }) return cmd @@ -646,3 +667,58 @@ func TestPortBindingWithCustomHost(t *testing.T) { testCase.Run(t) } + +func TestRunDeviceCDI(t *testing.T) { + t.Parallel() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testutil.DockerIncompatible(t) + cdiSpecDir := filepath.Join(t.TempDir(), "cdi") + writeTestCDISpec(t, cdiSpecDir) + + base := testutil.NewBase(t) + base.Cmd("--cdi-spec-dirs", cdiSpecDir, "run", + "--rm", + "--device", "vendor1.com/device=foo", + testutil.AlpineImage, "env", + ).AssertOutContains("FOO=injected") +} + +func TestRunDeviceCDIWithNerdctlConfig(t *testing.T) { + t.Parallel() + // Although CDI injection is supported by Docker, specifying the --cdi-spec-dirs on the command line is not. + testutil.DockerIncompatible(t) + cdiSpecDir := filepath.Join(t.TempDir(), "cdi") + writeTestCDISpec(t, cdiSpecDir) + + tomlPath := filepath.Join(t.TempDir(), "nerdctl.toml") + err := os.WriteFile(tomlPath, []byte(fmt.Sprintf(` +cdi_spec_dirs = ["%s"] +`, cdiSpecDir)), 0400) + assert.NilError(t, err) + + base := testutil.NewBase(t) + base.Env = append(base.Env, "NERDCTL_TOML="+tomlPath) + base.Cmd("run", + "--rm", + "--device", "vendor1.com/device=foo", + testutil.AlpineImage, "env", + ).AssertOutContains("FOO=injected") +} + +func writeTestCDISpec(t *testing.T, cdiSpecDir string) { + const testCDIVendor1 = ` +cdiVersion: "0.3.0" +kind: "vendor1.com/device" +devices: +- name: foo + containerEdits: + env: + - FOO=injected +` + + err := os.MkdirAll(cdiSpecDir, 0700) + assert.NilError(t, err) + cdiSpecPath := filepath.Join(cdiSpecDir, "vendor1.yaml") + err = os.WriteFile(cdiSpecPath, []byte(testCDIVendor1), 0400) + assert.NilError(t, err) +} diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index f02d47d5198..397ccf12969 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -27,6 +27,8 @@ import ( "gotest.tools/v3/assert" "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -304,68 +306,105 @@ func TestRunBindMountTmpfs(t *testing.T) { base.Cmd("run", "--rm", "--mount", "type=tmpfs,target=/tmp,tmpfs-size=64m", testutil.AlpineImage, "grep", "/tmp", "/proc/mounts").AssertOutWithFunc(f([]string{"rw", "nosuid", "nodev", "size=65536k"})) } -func TestRunBindMountBind(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) - } +func mountExistsWithOpt(mountPoint, mountOpt string) test.Comparator { + return func(stdout, info string, t *testing.T) { + lines := strings.Split(strings.TrimSpace(stdout), "\n") + mountOutput := []string{} + for _, line := range lines { + if strings.Contains(line, mountPoint) { + mountOutput = strings.Split(line, " ") + break + } + } - containerName := tID - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", - "-d", - "--name", containerName, - "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir), - "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt2,ro", roDir), - testutil.AlpineImage, - "top", - ).AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str1 > /mnt1/file1").AssertOK() - base.Cmd("exec", containerName, "sh", "-exc", "echo -n str2 > /mnt2/file2").AssertFail() + assert.Assert(t, len(mountOutput) > 0, "we should have found the mount point in /proc/mounts") + assert.Assert(t, len(mountOutput) >= 4, "invalid format for mount line") - base.Cmd("run", - "--rm", - "--mount", fmt.Sprintf("type=bind,src=%s,target=/mnt1", rwDir), - testutil.AlpineImage, - "cat", "/mnt1/file1", - ).AssertOutExactly("str1") + options := strings.Split(mountOutput[3], ",") - // check `bind-propagation` - f := func(allow string) func(stdout string) error { - return func(stdout string) error { - lines := strings.Split(strings.TrimSpace(stdout), "\n") - if len(lines) != 1 { - return fmt.Errorf("expected 1 lines, got %q", stdout) - } - fields := strings.Split(lines[0], " ") - if len(fields) < 4 { - return fmt.Errorf("invalid /proc/mounts format %q", stdout) + found := false + for _, opt := range options { + if mountOpt == opt { + found = true + break } + } + + assert.Assert(t, found, "mount option %s not found", mountOpt) + } +} - options := strings.Split(fields[3], ",") +func TestRunBindMountBind(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + // Run a container with bind mount directories, one rw, the other ro + rwDir := data.Temp().Dir("rw") + roDir := data.Temp().Dir("ro") + + helpers.Ensure( + "run", + "-d", + "--name", data.Identifier("container"), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntrw", rwDir), + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntro,ro", roDir), + testutil.AlpineImage, + "top", + ) + + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container")) + + // Save host rwDir location and container id for subtests + data.Labels().Set("container", data.Identifier("container")) + data.Labels().Set("rwDir", rwDir) + } - found := false - for _, s := range options { - if allow == s { - found = true - break + testCase.SubTests = []*test.Case{ + { + Description: "ensure we cannot write to ro mount", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container"), "sh", "-exc", "echo -n failure > /mntro/file") + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "ensure we can write to rw, and read it back from another container mounting the same target", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("exec", data.Labels().Get("container"), "sh", "-exc", "echo -n success > /mntrw/file") + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "run", + "--rm", + "--mount", fmt.Sprintf("type=bind,src=%s,target=/mntrw", data.Labels().Get("rwDir")), + testutil.AlpineImage, + "cat", "/mntrw/file", + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("success")), + }, + { + Description: "Check that mntrw is seen in /proc/mounts", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container"), "cat", "/proc/mounts") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: expect.All( + // Ensure we have mntrw in the mount list + mountExistsWithOpt("/mntrw", "rw"), + mountExistsWithOpt("/mntro", "ro"), + ), } - } - if !found { - return fmt.Errorf("expected stdout to contain %q, got %+v", allow, options) - } - return nil - } + }, + }, } - base.Cmd("exec", containerName, "grep", "/mnt1", "/proc/mounts").AssertOutWithFunc(f("rw")) - base.Cmd("exec", containerName, "grep", "/mnt2", "/proc/mounts").AssertOutWithFunc(f("ro")) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("container")) + } + + testCase.Run(t) } func TestRunMountBindMode(t *testing.T) { diff --git a/cmd/nerdctl/container/container_run_network_linux_test.go b/cmd/nerdctl/container/container_run_network_linux_test.go index e275669fa38..f8a93aaa6a2 100644 --- a/cmd/nerdctl/container/container_run_network_linux_test.go +++ b/cmd/nerdctl/container/container_run_network_linux_test.go @@ -41,7 +41,6 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" - "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" @@ -362,11 +361,8 @@ func TestRunWithInvalidPortThenCleanUp(t *testing.T) { 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) + return helpers.Command("run", "--data-root", data.Temp().Path(), "--rm", "-p", "22200-22299:22200-22299", testutil.CommonImage) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -382,7 +378,7 @@ func TestRunWithInvalidPortThenCleanUp(t *testing.T) { return h } - dataRoot := data.TempDir() + dataRoot := data.Temp().Path() h := getAddrHash(defaults.DefaultAddress) dataStore := filepath.Join(dataRoot, h) namespace := string(helpers.Read(nerdtest.Namespace)) @@ -519,158 +515,104 @@ func TestSharedNetworkSetup(t *testing.T) { testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - data.Set("containerName1", data.Identifier("-container1")) - containerName1 := data.Get("containerName1") - helpers.Ensure("run", "-d", "--name", containerName1, - testutil.NginxAlpineImage) + data.Labels().Set("container1", data.Identifier("container1")) + helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), + testutil.CommonImage, "sleep", "inf") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier("-container1")) + helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, SubTests: []*test.Case{ { Description: "Test network is shared", NoParallel: true, // The validation involves starting of the main container: container1 Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Identifier("container2")) }, - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, - "--network=container:"+data.Get("containerName1"), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure( + "run", "-d", "--name", data.Identifier("container2"), + "--network=container:"+data.Labels().Get("container1"), testutil.NginxAlpineImage) - return cmd + data.Labels().Set("container2", data.Identifier("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container2")) }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - containerName2 := data.Identifier() - assert.Assert(t, strings.Contains(helpers.Capture("exec", containerName2, "wget", "-qO-", "http://127.0.0.1:80"), testutil.NginxAlpineIndexHTMLSnippet), info) - helpers.Ensure("restart", data.Get("containerName1")) - helpers.Ensure("stop", "--time=1", containerName2) - helpers.Ensure("start", containerName2) - assert.Assert(t, strings.Contains(helpers.Capture("exec", containerName2, "wget", "-qO-", "http://127.0.0.1:80"), testutil.NginxAlpineIndexHTMLSnippet), info) + SubTests: []*test.Case{ + { + NoParallel: true, + Description: "Test network is shared", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") + }, - } + Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), + }, + { + NoParallel: true, + Description: "Test network is shared after restart", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("restart", data.Labels().Get("container1")) + helpers.Ensure("stop", "--time=1", data.Labels().Get("container2")) + helpers.Ensure("start", data.Labels().Get("container2")) + nerdtest.EnsureContainerStarted(helpers, data.Labels().Get("container2")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("exec", data.Labels().Get("container2"), "wget", "-qO-", "http://127.0.0.1:80") + + }, + Expected: test.Expects(0, nil, expect.Contains(testutil.NginxAlpineIndexHTMLSnippet)), + }, }, }, { Description: "Test uts is supported in shared network", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--uts", "host", - "--network=container:"+data.Get("containerName1"), - testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - } + return helpers.Command("run", "--rm", "--uts", "host", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage) }, + Expected: test.Expects(0, nil, nil), }, { Description: "Test dns is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--dns", "0.1.2.3", - "--network=container:"+data.Get("containerName1"), - testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } + return helpers.Command("run", "--rm", "--dns", "0.1.2.3", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage) }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test dns options is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "--name", containerName2, "--dns-option", "attempts:5", - "--network=container:"+data.Get("containerName1"), - testutil.AlpineImage, "cat", "/etc/resolv.conf") - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - // The Option doesnt throw an error but is never inserted to the resolv.conf - return &test.Expected{ - ExitCode: 0, - Output: func(stdout string, info string, t *testing.T) { - assert.Assert(t, !strings.Contains(stdout, "attempts:5"), info) - }, - } + return helpers.Command("run", "--rm", "--dns-option", "attempts:5", + "--network=container:"+data.Labels().Get("container1"), + testutil.CommonImage, "cat", "/etc/resolv.conf") }, + // The Option doesn't throw an error but is never inserted to the resolv.conf + Expected: test.Expects(0, nil, expect.DoesNotContain("attempts:5")), }, { Description: "Test publish is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--publish", "80:8080", - "--network=container:"+data.Get("containerName1"), + return helpers.Command("run", "--rm", "--publish", "80:8080", + "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, { Description: "Test hostname is not supported", - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, "--hostname", "test", - "--network=container:"+data.Get("containerName1"), + return helpers.Command("run", "--rm", "--hostname", "test", + "--network=container:"+data.Labels().Get("container1"), testutil.AlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - if nerdtest.IsDocker() { - return &test.Expected{ - ExitCode: 125, - } - - } - return &test.Expected{ - ExitCode: 1, - } }, + // 1 for nerdctl, 125 for docker + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), }, }, } @@ -682,27 +624,18 @@ func TestSharedNetworkWithNone(t *testing.T) { testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - data.Set("containerName1", data.Identifier("-container1")) - containerName1 := data.Get("containerName1") - helpers.Ensure("run", "-d", "--name", containerName1, "--network", "none", - testutil.NginxAlpineImage) + helpers.Ensure("run", "-d", "--name", data.Identifier("container1"), "--network", "none", + testutil.CommonImage, "sleep", "inf") + nerdtest.EnsureContainerStarted(helpers, data.Identifier("container1")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Get("containerName1")) + helpers.Anyhow("rm", "-f", data.Identifier("container1")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - containerName2 := data.Identifier() - cmd := helpers.Command() - cmd.WithArgs("run", "-d", "--name", containerName2, - "--network=container:"+data.Get("containerName1"), - testutil.NginxAlpineImage) - return cmd - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: 0, - } + return helpers.Command("run", "--rm", + "--network=container:"+data.Identifier("container1"), testutil.CommonImage) }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), } testCase.Run(t) } @@ -905,7 +838,7 @@ func TestRunContainerWithStaticIP6(t *testing.T) { return } cmd.AssertOutWithFunc(func(stdout string) error { - ip := helpers.FindIPv6(stdout) + ip := nerdtest.FindIPv6(stdout) if !subnet.Contains(ip) { return fmt.Errorf("expected subnet %s include ip %s", subnet, ip) } @@ -924,20 +857,21 @@ func TestNoneNetworkHostName(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - output := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", "none", testutil.NginxAlpineImage) - assert.Assert(helpers.T(), len(output) > 12, output) - data.Set("hostname", output[:12]) - }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) }, + Setup: func(data test.Data, helpers test.Helpers) { + output := helpers.Capture("run", "-d", "--name", data.Identifier(), "--network", "none", testutil.CommonImage, "sleep", "inf") + assert.Assert(helpers.T(), len(output) > 12, output) + data.Labels().Set("hostname", output[:12]) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("exec", data.Identifier(), "cat", "/etc/hostname") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Equals(data.Get("hostname") + "\n"), + Output: expect.Equals(data.Labels().Get("hostname") + "\n"), } }, } @@ -949,20 +883,20 @@ func TestHostNetworkHostName(t *testing.T) { testCase := &test.Case{ Require: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - data.Set("containerName1", data.Identifier()) - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Custom("cat", "/etc/hostname").Run(&test.Expected{ + Output: func(stdout, info string, t *testing.T) { + data.Labels().Set("hostHostname", stdout) + }, + }) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("cat", "/etc/hostname") + return helpers.Command("run", "--rm", + "--network", "host", + testutil.AlpineImage, "cat", "/etc/hostname") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - hostname := stdout - assert.Assert(t, strings.Compare(strings.TrimSpace(helpers.Capture("run", "--name", data.Identifier(), "--network", "host", testutil.AlpineImage, "cat", "/etc/hostname")), strings.TrimSpace(hostname)) == 0, info) - }, + Output: expect.Equals(data.Labels().Get("hostHostname")), } }, } @@ -973,27 +907,18 @@ func TestNoneNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("containerName1", data.Identifier()) - }, - 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("run", "-d", "--name", data.Identifier(), "--network", "none", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - out := helpers.Capture("exec", data.Identifier(), "cat", "/etc/resolv.conf") - assert.Assert(t, strings.Contains(out, "0.1.2.3"), info) - assert.Assert(t, strings.Contains(out, "example.com"), info) - assert.Assert(t, strings.Contains(out, "attempts:5"), info) - assert.Assert(t, strings.Contains(out, "timeout:3"), info) - - }, - } - }, + return helpers.Command("run", "--rm", + "--network", "none", + "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", + testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(0, nil, expect.Contains( + "0.1.2.3", + "example.com", + "attempts:5", + "timeout:3", + )), } testCase.Run(t) } @@ -1002,27 +927,18 @@ func TestHostNetworkDnsConfigs(t *testing.T) { nerdtest.Setup() testCase := &test.Case{ Require: require.Not(require.Windows), - Setup: func(data test.Data, helpers test.Helpers) { - data.Set("containerName1", data.Identifier()) - }, - 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("run", "-d", "--name", data.Identifier(), "--network", "host", "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", testutil.NginxAlpineImage) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - Output: func(stdout string, info string, t *testing.T) { - out := helpers.Capture("exec", data.Identifier(), "cat", "/etc/resolv.conf") - assert.Assert(t, strings.Contains(out, "0.1.2.3"), info) - assert.Assert(t, strings.Contains(out, "example.com"), info) - assert.Assert(t, strings.Contains(out, "attempts:5"), info) - assert.Assert(t, strings.Contains(out, "timeout:3"), info) - - }, - } - }, + return helpers.Command("run", "--rm", + "--network", "host", + "--dns", "0.1.2.3", "--dns-search", "example.com", "--dns-option", "timeout:3", "--dns-option", "attempts:5", + testutil.CommonImage, "cat", "/etc/resolv.conf") + }, + Expected: test.Expects(0, nil, expect.Contains( + "0.1.2.3", + "example.com", + "attempts:5", + "timeout:3", + )), } testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_run_windows.go b/cmd/nerdctl/container/container_run_nolinux.go similarity index 97% rename from cmd/nerdctl/container/container_run_windows.go rename to cmd/nerdctl/container/container_run_nolinux.go index 5ef9a6d94fb..334461c583e 100644 --- a/cmd/nerdctl/container/container_run_windows.go +++ b/cmd/nerdctl/container/container_run_nolinux.go @@ -1,3 +1,5 @@ +//go:build !linux + /* Copyright The containerd Authors. diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index c3411c6aaeb..a5ce810bbad 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -28,6 +28,7 @@ import ( "gotest.tools/v3/poll" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" ) @@ -92,7 +93,7 @@ func TestRunRestart(t *testing.T) { func TestRunRestartWithOnFailure(t *testing.T) { base := testutil.NewBase(t) - if testutil.GetTarget() == testutil.Nerdctl { + if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) @@ -113,7 +114,7 @@ func TestRunRestartWithOnFailure(t *testing.T) { func TestRunRestartWithUnlessStopped(t *testing.T) { base := testutil.NewBase(t) - if testutil.GetTarget() == testutil.Nerdctl { + if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"unless-stopped"}) } tID := testutil.Identifier(t) @@ -137,7 +138,7 @@ func TestRunRestartWithUnlessStopped(t *testing.T) { func TestUpdateRestartPolicy(t *testing.T) { base := testutil.NewBase(t) - if testutil.GetTarget() == testutil.Nerdctl { + if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) @@ -160,7 +161,7 @@ func TestUpdateRestartPolicy(t *testing.T) { // and check it can work correctly. func TestAddRestartPolicy(t *testing.T) { base := testutil.NewBase(t) - if testutil.GetTarget() == testutil.Nerdctl { + if !nerdtest.IsDocker() { testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) } tID := testutil.Identifier(t) diff --git a/cmd/nerdctl/container/container_run_soci_linux_test.go b/cmd/nerdctl/container/container_run_soci_linux_test.go index 57cf0599525..670a15dc7de 100644 --- a/cmd/nerdctl/container/container_run_soci_linux_test.go +++ b/cmd/nerdctl/container/container_run_soci_linux_test.go @@ -17,60 +17,63 @@ package container import ( - "os/exec" + "strconv" "strings" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) 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, - }, - } + testCase := nerdtest.Setup() - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - base := testutil.NewBase(t) - helpers.RequiresSoci(base) + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.Soci, + ) - //counting initial snapshot mounts - initialMounts, err := exec.Command("mount").Output() - if err != nil { - t.Fatal(err) - } + // Tests relying on the output of "mount" cannot be run in parallel obviously + testCase.NoParallel = true - remoteSnapshotsInitialCount := strings.Count(string(initialMounts), "fuse.rawBridge") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Custom("mount").Run(&test.Expected{ + ExitCode: 0, + Output: func(stdout, info string, t *testing.T) { + data.Labels().Set("beforeCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) + }, + }) + } - runOutput := base.Cmd("--snapshotter=soci", "run", "--rm", testutil.FfmpegSociImage).Out() - base.T.Logf("run output: %s", runOutput) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", testutil.FfmpegSociImage) + } - 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) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("--snapshotter=soci", "run", "--rm", testutil.FfmpegSociImage) + } - rmiOutput := base.Cmd("rmi", testutil.FfmpegSociImage).Out() - base.T.Logf("rmi output: %s", rmiOutput) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout, info string, t *testing.T) { + var afterCount int + beforeCount, _ := strconv.Atoi(data.Labels().Get("beforeCount")) - base.T.Logf("number of expected mounts: %v", tt.remoteSnapshotsExpectedCount) + helpers.Custom("mount").Run(&test.Expected{ + Output: func(stdout, info string, t *testing.T) { + afterCount = strings.Count(stdout, "fuse.rawBridge") + }, + }) - if tt.remoteSnapshotsExpectedCount != (remoteSnapshotsActualCount - remoteSnapshotsInitialCount) { - t.Fatalf("incorrect number of remote snapshots; expected=%d, actual=%d", - tt.remoteSnapshotsExpectedCount, remoteSnapshotsActualCount-remoteSnapshotsInitialCount) - } - }) + assert.Equal(t, 11, afterCount-beforeCount, "expected the number of fuse.rawBridge") + }, + } } + + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_run_stargz_linux_test.go b/cmd/nerdctl/container/container_run_stargz_linux_test.go index 3132b28f9b9..3298a492130 100644 --- a/cmd/nerdctl/container/container_run_stargz_linux_test.go +++ b/cmd/nerdctl/container/container_run_stargz_linux_test.go @@ -35,7 +35,7 @@ func TestRunStargz(t *testing.T) { require.Not(nerdtest.Docker), ) - testCase.Command = test.Command("--snapshotter=stargz", "run", "--rm", testutil.FedoraESGZImage, "ls", "/.stargz-snapshotter") + testCase.Command = test.Command("--snapshotter=stargz", "run", "--quiet", "--rm", testutil.FedoraESGZImage, "ls", "/.stargz-snapshotter") testCase.Expected = test.Expects(0, nil, nil) diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index 97eac527b8d..345523f1382 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -34,215 +34,276 @@ import ( "gotest.tools/v3/icmd" "gotest.tools/v3/poll" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "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) - imageName := testutil.Identifier(t) - defer base.Cmd("rmi", imageName).Run() + nerdtest.Setup() dockerfile := fmt.Sprintf(`FROM %s ENTRYPOINT ["echo", "foo"] CMD ["echo", "bar"] `, testutil.CommonImage) - buildCtx := helpers.CreateBuildContext(t, dockerfile) + testCase := &test.Case{ + Require: nerdtest.Build, + Setup: func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("image", data.Identifier()) + helpers.Ensure("build", "-t", data.Labels().Get("image"), data.Temp().Path()) + }, + SubTests: []*test.Case{ + { + Description: "Run image", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", data.Labels().Get("image")) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Equals("foo echo bar\n")), + }, + { + Description: "Run image empty entrypoint", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--entrypoint", "", data.Labels().Get("image")) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "Run image time entrypoint", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--entrypoint", "time", data.Labels().Get("image")) + }, + Expected: test.Expects(expect.ExitCodeGenericFail, nil, nil), + }, + { + Description: "Run image empty entrypoint custom command", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--entrypoint", "", data.Labels().Get("image"), "echo", "blah") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("blah"), + expect.DoesNotContain("foo", "bar"), + )), + }, + { + Description: "Run image time entrypoint custom command", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--entrypoint", "time", data.Labels().Get("image"), "echo", "blah") + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.All( + expect.Contains("blah"), + expect.DoesNotContain("foo", "bar"), + )), + }, + }, + } - base.Cmd("build", "-t", imageName, buildCtx).AssertOK() - base.Cmd("run", "--rm", imageName).AssertOutExactly("foo echo bar\n") - base.Cmd("run", "--rm", "--entrypoint", "", imageName).AssertFail() - base.Cmd("run", "--rm", "--entrypoint", "", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "blah") { - return errors.New("echo blah was not executed?") - } - if strings.Contains(stdout, "bar") { - return errors.New("echo bar should not be executed") - } - if strings.Contains(stdout, "foo") { - return errors.New("echo foo should not be executed") - } - return nil - }) - base.Cmd("run", "--rm", "--entrypoint", "time", imageName).AssertFail() - base.Cmd("run", "--rm", "--entrypoint", "time", imageName, "echo", "blah").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "blah") { - return errors.New("echo blah was not executed?") - } - if strings.Contains(stdout, "bar") { - return errors.New("echo bar should not be executed") - } - if strings.Contains(stdout, "foo") { - return errors.New("echo foo should not be executed") - } - return nil - }) + testCase.Run(t) } func TestRunWorkdir(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + dir := "/foo" if runtime.GOOS == "windows" { dir = "c:" + dir } - cmd := base.Cmd("run", "--rm", "--workdir="+dir, testutil.CommonImage, "pwd") - cmd.AssertOutContains("/foo") + + testCase.Command = test.Command("run", "--rm", "--workdir="+dir, testutil.CommonImage, "pwd") + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(dir)) + + testCase.Run(t) } func TestRunWithDoubleDash(t *testing.T) { - t.Parallel() - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - base.Cmd("run", "--rm", testutil.CommonImage, "--", "sh", "-euxc", "exit 0").AssertOK() + testCase := nerdtest.Setup() + + testCase.Require = require.Not(nerdtest.Docker) + + testCase.Command = test.Command("run", "--rm", testutil.CommonImage, "--", "sh", "-euxc", "exit 0") + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestRunExitCode(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - testContainer0 := tID + "-0" - testContainer123 := tID + "-123" - defer base.Cmd("rm", "-f", testContainer0, testContainer123).Run() - - base.Cmd("run", "--name", testContainer0, testutil.CommonImage, "sh", "-euxc", "exit 0").AssertOK() - base.Cmd("run", "--name", testContainer123, testutil.CommonImage, "sh", "-euxc", "exit 123").AssertExitCode(123) - base.Cmd("ps", "-a").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "Exited (0)") { - return fmt.Errorf("no entry for %q", testContainer0) - } - if !strings.Contains(stdout, "Exited (123)") { - return fmt.Errorf("no entry for %q", testContainer123) - } - return nil - }) + testCase := nerdtest.Setup() + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("exit0")) + helpers.Anyhow("rm", "-f", data.Identifier("exit123")) + } - inspect0 := base.InspectContainer(testContainer0) - assert.Equal(base.T, "exited", inspect0.State.Status) - assert.Equal(base.T, 0, inspect0.State.ExitCode) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--name", data.Identifier("exit0"), testutil.CommonImage, "sh", "-euxc", "exit 0") + helpers.Command("run", "--name", data.Identifier("exit123"), testutil.CommonImage, "sh", "-euxc", "exit 123"). + Run(&test.Expected{ExitCode: 123}) + } + + testCase.Command = test.Command("ps", "-a") - inspect123 := base.InspectContainer(testContainer123) - assert.Equal(base.T, "exited", inspect123.State.Status) - assert.Equal(base.T, 123, inspect123.State.ExitCode) + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Errors: nil, + Output: expect.All( + expect.Match(regexp.MustCompile("Exited [(]123[)][A-Za-z0-9 ]+"+data.Identifier("exit123"))), + expect.Match(regexp.MustCompile("Exited [(]0[)][A-Za-z0-9 ]+"+data.Identifier("exit0"))), + func(stdout, info string, t *testing.T) { + assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit0")).State.Status, "exited") + assert.Equal(t, nerdtest.InspectContainer(helpers, data.Identifier("exit123")).State.Status, "exited") + }, + ), + } + } + + testCase.Run(t) } func TestRunCIDFile(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - fileName := filepath.Join(t.TempDir(), "cid.file") + testCase := nerdtest.Setup() - base.Cmd("run", "--rm", "--cidfile", fileName, testutil.CommonImage).AssertOK() - defer os.Remove(fileName) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "--rm", "--cidfile", data.Temp().Path("cid-file"), testutil.CommonImage) + data.Temp().Exists("cid-file") + } - _, err := os.Stat(fileName) - assert.NilError(base.T, err) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--cidfile", data.Temp().Path("cid-file"), testutil.CommonImage) + } + + // Docker will return 125 while nerdctl returns 1, so, generic fail instead of specific exit code + testCase.Expected = test.Expects(expect.ExitCodeGenericFail, []error{errors.New("container ID file found")}, nil) - base.Cmd("run", "--rm", "--cidfile", fileName, testutil.CommonImage).AssertFail() + testCase.Run(t) } func TestRunEnvFile(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - base.Env = append(base.Env, "HOST_ENV=ENV-IN-HOST") - - tID := testutil.Identifier(t) - file1, err := os.CreateTemp("", tID) - assert.NilError(base.T, err) - path1 := file1.Name() - defer file1.Close() - defer os.Remove(path1) - err = os.WriteFile(path1, []byte("# this is a comment line\nTESTKEY1=TESTVAL1"), 0666) - assert.NilError(base.T, err) - - file2, err := os.CreateTemp("", tID) - assert.NilError(base.T, err) - path2 := file2.Name() - defer file2.Close() - defer os.Remove(path2) - err = os.WriteFile(path2, []byte("# this is a comment line\nTESTKEY2=TESTVAL2\nHOST_ENV"), 0666) - assert.NilError(base.T, err) - - base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $TESTKEY1").AssertOutExactly("TESTVAL1") - base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $TESTKEY2").AssertOutExactly("TESTVAL2") - base.Cmd("run", "--rm", "--env-file", path1, "--env-file", path2, testutil.CommonImage, "sh", "-c", "echo -n $HOST_ENV").AssertOutExactly("ENV-IN-HOST") + testCase := nerdtest.Setup() + + testCase.Env = map[string]string{ + "HOST_ENV": "ENV-IN-HOST", + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save("# this is a comment line\nTESTKEY1=TESTVAL1", "env1-file") + data.Temp().Save("# this is a comment line\nTESTKEY2=TESTVAL2\nHOST_ENV", "env2-file") + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "run", "--rm", + "--env-file", data.Temp().Path("env1-file"), + "--env-file", data.Temp().Path("env2-file"), + testutil.CommonImage, "env") + } + + testCase.Expected = test.Expects( + expect.ExitCodeSuccess, + nil, + expect.Contains("TESTKEY1=TESTVAL1", "TESTKEY2=TESTVAL2", "HOST_ENV=ENV-IN-HOST"), + ) + + testCase.Run(t) } func TestRunEnv(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - 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", - "--env", "BAZ=", - "--env", "QUX", // not exported in OS - "--env", "QUUX=quux1", - "--env", "QUUX=quux2", - "--env", "CORGE", // OS exported - "--env", "GRAULT=grault_key=grault_value", // value contains `=` char - "--env", "GARPLY=", // OS exported - "--env", "WALDO=", // not exported in OS - - testutil.CommonImage, "env").AssertOutWithFunc(func(stdout string) error { - if !strings.Contains(stdout, "\nFOO=foo1,foo2\n") { - return errors.New("got bad FOO") - } - if !strings.Contains(stdout, "\nBAR=bar1 bar2\n") { - return errors.New("got bad BAR") - } - if !strings.Contains(stdout, "\nBAZ=\n") && runtime.GOOS != "windows" { - return errors.New("got bad BAZ") - } - if strings.Contains(stdout, "QUX") { - return errors.New("got bad QUX (should not be set)") - } - if !strings.Contains(stdout, "\nQUUX=quux2\n") { - return errors.New("got bad QUUX") - } - if !strings.Contains(stdout, "\nCORGE=corge-value-in-host\n") { - return errors.New("got bad CORGE") - } - 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" { - return errors.New("got bad GARPLY") - } - if !strings.Contains(stdout, "\nWALDO=\n") && runtime.GOOS != "windows" { - return errors.New("got bad WALDO") - } - - return nil - }) + testCase := nerdtest.Setup() + + testCase.Env = map[string]string{ + "CORGE": "corge-value-in-host", + "GARPLY": "garply-value-in-host", + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--env", "FOO=foo1,foo2", + "--env", "BAR=bar1 bar2", + "--env", "BAZ=", + "--env", "QUX", // not exported in OS + "--env", "QUUX=quux1", + "--env", "QUUX=quux2", + "--env", "CORGE", // OS exported + "--env", "GRAULT=grault_key=grault_value", // value contains `=` char + "--env", "GARPLY=", // OS exported + "--env", "WALDO=", // not exported in OS + testutil.CommonImage, "env") + } + + validate := []test.Comparator{ + expect.Contains( + "\nFOO=foo1,foo2\n", + "\nBAR=bar1 bar2\n", + "\nQUUX=quux2\n", + "\nCORGE=corge-value-in-host\n", + "\nGRAULT=grault_key=grault_value\n", + ), + expect.DoesNotContain("QUX"), + } + + if runtime.GOOS != "windows" { + validate = append( + validate, + expect.Contains( + "\nBAZ=\n", + "\nGARPLY=\n", + "\nWALDO=\n", + ), + ) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.All(validate...)) + + testCase.Run(t) } -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() +func TestRunHostnameEnv(t *testing.T) { + testCase := nerdtest.Setup() - if runtime.GOOS == "windows" { - t.Skip("run --hostname not implemented on Windows yet") + testCase.SubTests = []*test.Case{ + { + Description: "default hostname", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "--quiet", testutil.CommonImage) + // Note: on Windows, just straight passing the command will not work (some cmd escaping weirdness?) + cmd.Feed(strings.NewReader(`[[ "HOSTNAME=$(hostname)" == "$(env | grep HOSTNAME)" ]]`)) + return cmd + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + }, + { + Description: "with --hostname", + // Windows does not support --hostname + Require: require.Not(require.Windows), + Command: test.Command("run", "--rm", "--quiet", "--hostname", "foobar", testutil.CommonImage, "env"), + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains("HOSTNAME=foobar")), + }, } - base.Cmd("run", "--rm", "--hostname", "foobar", testutil.CommonImage, "env").AssertOutContains("HOSTNAME=foobar") + + testCase.Run(t) } func TestRunStdin(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) + testCase := nerdtest.Setup() const testStr = "test-run-stdin" - opts := []func(*testutil.Cmd){ - testutil.WithStdin(strings.NewReader(testStr)), + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command("run", "--rm", "-i", testutil.CommonImage, "cat") + cmd.Feed(strings.NewReader(testStr)) + return cmd } - base.Cmd("run", "--rm", "-i", testutil.CommonImage, "cat").CmdOption(opts...).AssertOutExactly(testStr) + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Equals(testStr)) + + testCase.Run(t) } func TestRunWithJsonFileLogDriver(t *testing.T) { @@ -467,6 +528,8 @@ RUN echo '\ RUN go mod init +# Workaround for "package slices is not in GOROOT" https://github.com/containerd/nerdctl/issues/4214 +RUN go get github.com/containerd/containerd/v2@v2.0.5 RUN go mod tidy RUN go build . @@ -493,7 +556,7 @@ COPY --from=builder /go/src/logger/logger / } // 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. +// This test ensures that it doesn't happen. // (https://github.com/containerd/nerdctl/issues/2560) func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { if runtime.GOOS == "windows" { @@ -539,7 +602,11 @@ func TestRunRmTime(t *testing.T) { base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK() t1 := time.Now() took := t1.Sub(t0) - const deadline = 3 * time.Second + var deadline = 3 * time.Second + // FIXME: Investigate? it appears that since the move to containerd 2 on Windows, this is taking longer. + if runtime.GOOS == "windows" { + deadline = 10 * time.Second + } if took > deadline { t.Fatalf("expected to have completed in %v, took %v", deadline, took) } @@ -656,7 +723,7 @@ func TestRunAttachFlag(t *testing.T) { 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 { + if nerdtest.IsDocker() { assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg) } else { assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg) @@ -683,7 +750,7 @@ func TestRunQuiet(t *testing.T) { } // Docker and nerdctl image pulls are not 1:1. - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { sentinel = "Pull complete" } else { sentinel = "resolved" diff --git a/cmd/nerdctl/container/container_run_user_linux_test.go b/cmd/nerdctl/container/container_run_user_linux_test.go index 968057f1404..61e2d674d77 100644 --- a/cmd/nerdctl/container/container_run_user_linux_test.go +++ b/cmd/nerdctl/container/container_run_user_linux_test.go @@ -17,9 +17,16 @@ package container import ( + "fmt" "testing" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunUserGID(t *testing.T) { @@ -181,3 +188,222 @@ func TestRunAddGroup_CVE_2023_25173(t *testing.T) { base.Cmd(cmd...).AssertOutContains(testCase.expected + "\n") } } + +func TestUsernsMappingRunCmd(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Require: require.All( + nerdtest.AllowModifyUserns, + nerdtest.RemapIDs, + require.Not(nerdtest.Docker), + ), + Setup: func(data test.Data, helpers test.Helpers) { + data.Labels().Set("validUserns", "nerdctltestuser") + data.Labels().Set("expectedHostUID", "123456789") + data.Labels().Set("validUid", "123456789") + data.Labels().Set("net-container", "net-container") + data.Labels().Set("invalidUserns", "invaliduser") + }, + SubTests: []*test.Case{ + { + Description: "Test container run with valid Userns format userns username", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container run with valid Userns --userns uid", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUid"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container run failure with valid Userns and privileged flag", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "--privileged", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 1, + } + }, + }, + { + Description: "Test container run with valid Userns format --userns :", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", fmt.Sprintf("%s:%s", data.Labels().Get("validUserns"), data.Labels().Get("validUserns")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container run with valid Userns --userns uid:gid", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", fmt.Sprintf("%s:%s", data.Labels().Get("validUid"), data.Labels().Get("validUid")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == data.Labels().Get("expectedHostUID"), info) + }, + } + }, + }, + { + Description: "Test container network share with valid Userns", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + helpers.Ensure("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--name", data.Labels().Get("net-container"), testutil.CommonImage, "sleep", "inf") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rm", "-f", data.Labels().Get("net-container")) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--net", fmt.Sprintf("container:%s", data.Labels().Get("net-container")), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: test.Expects(0, nil, nil), + }, + { + Description: "Test container run with valid Userns with override --userns=host", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--userns", "host", "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: 0, + Output: func(stdout string, info string, t *testing.T) { + actualHostUID, err := getContainerHostUID(helpers, data.Identifier()) + if err != nil { + t.Fatalf("Failed to get container host UID: %v", err) + } + assert.Assert(t, actualHostUID == "0", info) + }, + } + }, + }, + { + Description: "Test container run with valid Userns with invalid overrid --userns=hostinvalid", + NoParallel: true, // Changes system config so running in non parallel mode + Setup: func(data test.Data, helpers test.Helpers) { + err := appendUsernsConfig(data.Labels().Get("validUserns"), data.Labels().Get("expectedHostUID"), helpers) + assert.NilError(t, err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + removeUsernsConfig(t, data.Labels().Get("validUserns"), helpers) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--tty", "-d", "--userns-remap", data.Labels().Get("validUserns"), "--userns", "hostinvalid", "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: test.Expects(1, nil, nil), + }, + { + Description: "Test container run with invalid Userns", + 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("run", "--tty", "-d", "--userns-remap", data.Labels().Get("invalidUserns"), "--name", data.Identifier(), testutil.CommonImage, "sleep", "inf") + }, + Expected: test.Expects(1, nil, nil), + }, + }, + } + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run_verify_linux_test.go b/cmd/nerdctl/container/container_run_verify_linux_test.go index 7d12342cbb3..e5d56373089 100644 --- a/cmd/nerdctl/container/container_run_verify_linux_test.go +++ b/cmd/nerdctl/container/container_run_verify_linux_test.go @@ -20,38 +20,66 @@ import ( "fmt" "testing" - "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/mod/tigron/expect" + "github.com/containerd/nerdctl/mod/tigron/require" + "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/testutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestRunVerifyCosign(t *testing.T) { - testutil.RequireExecutable(t, "cosign") - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - t.Parallel() - - base := testutil.NewBase(t) - 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 := helpers.CreateBuildContext(t, dockerfile) + testCase := nerdtest.Setup() + + var reg *registry.Server + + testCase.Require = require.All( + require.Binary("cosign"), + require.Not(nerdtest.Docker), + nerdtest.Build, + nerdtest.Registry, + ) + + testCase.Env["COSIGN_PASSWORD"] = "1" + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + pri, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "1") + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + + testImageRef := fmt.Sprintf("127.0.0.1:%d/%s", reg.Port, data.Identifier("push-cosign-image")) + helpers.Ensure("build", "-t", testImageRef, data.Temp().Path()) + helpers.Ensure("push", testImageRef, "--sign=cosign", "--cosign-key="+pri) + + data.Labels().Set("public_key", pub) + data.Labels().Set("image_ref", testImageRef) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + helpers.Fail( + "run", "--rm", "--verify=cosign", + "--cosign-key=dummy", + data.Labels().Get("image_ref")) + + return helpers.Command( + "run", "--rm", "--verify=cosign", + "--cosign-key="+data.Labels().Get("public_key"), + data.Labels().Get("image_ref")) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) - 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("run", "--rm", "--verify=cosign", "--cosign-key=dummy", testImageRef).AssertFail() + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_start.go b/cmd/nerdctl/container/container_start.go index 42673f46c64..7b770d9b1e6 100644 --- a/cmd/nerdctl/container/container_start.go +++ b/cmd/nerdctl/container/container_start.go @@ -43,7 +43,7 @@ func StartCommand() *cobra.Command { cmd.Flags().SetInterspersed(false) cmd.Flags().BoolP("attach", "a", false, "Attach STDOUT/STDERR and forward signals") cmd.Flags().String("detach-keys", consoleutil.DefaultDetachKeys, "Override the default detach keys") - + cmd.Flags().BoolP("interactive", "i", false, "Attach container's STDIN") return cmd } @@ -60,11 +60,16 @@ func startOptions(cmd *cobra.Command) (types.ContainerStartOptions, error) { if err != nil { return types.ContainerStartOptions{}, err } + interactive, err := cmd.Flags().GetBool("interactive") + if err != nil { + return types.ContainerStartOptions{}, err + } return types.ContainerStartOptions{ - Stdout: cmd.OutOrStdout(), - GOptions: globalOptions, - Attach: attach, - DetachKeys: detachKeys, + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Attach: attach, + DetachKeys: detachKeys, + Interactive: interactive, }, nil } diff --git a/cmd/nerdctl/container/container_start_linux_test.go b/cmd/nerdctl/container/container_start_linux_test.go index afd7d1d788b..6d9ca8c313b 100644 --- a/cmd/nerdctl/container/container_start_linux_test.go +++ b/cmd/nerdctl/container/container_start_linux_test.go @@ -17,8 +17,9 @@ package container import ( + "bytes" "errors" - "os" + "io" "strings" "testing" @@ -40,10 +41,8 @@ func TestStartDetachKeys(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--name", data.Identifier(), testutil.CommonImage) - cmd.WithPseudoTTY(func(f *os.File) error { - _, err := f.WriteString("exit\n") - return err - }) + cmd.WithPseudoTTY() + cmd.Feed(strings.NewReader("exit\n")) cmd.Run(&test.Expected{ ExitCode: 0, }) @@ -53,17 +52,11 @@ func TestStartDetachKeys(t *testing.T) { } testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { - flags := "-a" - // Started container must be interactive - which is apparently the default for nerdctl, which does not support - // the -i flag, while docker requires it explicitly - if nerdtest.IsDocker() { - flags += "i" - } - cmd := helpers.Command("start", flags, "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) - cmd.WithPseudoTTY(func(f *os.File) error { + cmd := helpers.Command("start", "-ai", "--detach-keys=ctrl-a,ctrl-b", data.Identifier()) + cmd.WithPseudoTTY() + cmd.WithFeeder(func() io.Reader { // ctrl+a and ctrl+b (see https://en.wikipedia.org/wiki/C0_and_C1_control_codes) - _, err := f.Write([]byte{1, 2}) - return err + return bytes.NewReader([]byte{1, 2}) }) return cmd diff --git a/cmd/nerdctl/container/container_stats_test.go b/cmd/nerdctl/container/container_stats_test.go index 0648d502d94..59132d156a1 100644 --- a/cmd/nerdctl/container/container_stats_test.go +++ b/cmd/nerdctl/container/container_stats_test.go @@ -53,7 +53,7 @@ func TestStats(t *testing.T) { 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")) + data.Labels().Set("id", data.Identifier("container")) } testCase.SubTests = []*test.Case{ @@ -62,7 +62,7 @@ func TestStats(t *testing.T) { Command: test.Command("stats", "--no-stream", "--no-trunc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("id")), + Output: expect.Contains(data.Labels().Get("id")), } }, }, @@ -71,21 +71,21 @@ func TestStats(t *testing.T) { Command: test.Command("container", "stats", "--no-stream", "--no-trunc"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("id")), + Output: expect.Contains(data.Labels().Get("id")), } }, }, { Description: "stats ID", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("stats", "--no-stream", data.Get("id")) + return helpers.Command("stats", "--no-stream", data.Labels().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")) + return helpers.Command("container", "stats", "--no-stream", data.Labels().Get("id")) }, Expected: test.Expects(0, nil, nil), }, diff --git a/cmd/nerdctl/container/container_stop_linux_test.go b/cmd/nerdctl/container/container_stop_linux_test.go index c9ba8874af1..135da90a938 100644 --- a/cmd/nerdctl/container/container_stop_linux_test.go +++ b/cmd/nerdctl/container/container_stop_linux_test.go @@ -130,7 +130,7 @@ func TestStopCleanupForwards(t *testing.T) { // define iptables chain name depending on the target (docker/nerdctl) var chain string - if testutil.GetTarget() == testutil.Docker { + if nerdtest.IsDocker() { chain = "DOCKER" } else { redirectChain := "CNI-HOSTPORT-DNAT" diff --git a/cmd/nerdctl/container/container_top.go b/cmd/nerdctl/container/container_top.go index d6ec4702260..6c802ce841a 100644 --- a/cmd/nerdctl/container/container_top.go +++ b/cmd/nerdctl/container/container_top.go @@ -19,6 +19,7 @@ package container import ( "errors" "fmt" + "strings" "github.com/spf13/cobra" @@ -66,9 +67,18 @@ func topAction(cmd *cobra.Command, args []string) error { return err } defer cancel() - return container.Top(ctx, client, args, types.ContainerTopOptions{ + + containerID := args[0] + var psArgs string + if len(args) > 1 { + // Join all remaining arguments as ps args + psArgs = strings.Join(args[1:], " ") + } + + return container.Top(ctx, client, []string{containerID}, types.ContainerTopOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, + PsArgs: psArgs, }) } diff --git a/cmd/nerdctl/container/container_top_test.go b/cmd/nerdctl/container/container_top_test.go index 6f18721b14e..e63d71f4150 100644 --- a/cmd/nerdctl/container/container_top_test.go +++ b/cmd/nerdctl/container/container_top_test.go @@ -38,7 +38,7 @@ func TestTop(t *testing.T) { 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()) + data.Labels().Set("cID", data.Identifier()) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -51,7 +51,7 @@ func TestTop(t *testing.T) { // Docker does not support top -o Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("top", data.Get("cID"), "-o", "pid,user,cmd") + return helpers.Command("top", data.Labels().Get("cID"), "-o", "pid,user,cmd") }, Expected: test.Expects(0, nil, nil), @@ -59,7 +59,7 @@ func TestTop(t *testing.T) { { Description: "simple", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("top", data.Get("cID")) + return helpers.Command("top", data.Labels().Get("cID")) }, Expected: test.Expects(0, nil, nil), diff --git a/cmd/nerdctl/container/container_update.go b/cmd/nerdctl/container/container_update.go index fb728655d29..28ac0f6d078 100644 --- a/cmd/nerdctl/container/container_update.go +++ b/cmd/nerdctl/container/container_update.go @@ -38,7 +38,7 @@ import ( "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" + 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" @@ -358,7 +358,7 @@ func updateContainer(ctx context.Context, client *containerd.Client, id string, return err } if cmd.Flags().Changed("restart") && restart != "" { - if err := nerdctlContainer.UpdateContainerRestartPolicyLabel(ctx, client, container, restart); err != nil { + if err := nerdctlcontainer.UpdateContainerRestartPolicyLabel(ctx, client, container, restart); err != nil { return err } } diff --git a/cmd/nerdctl/helpers/flagutil.go b/cmd/nerdctl/helpers/flagutil.go index f1137ac5ebc..4f9261c0cf3 100644 --- a/cmd/nerdctl/helpers/flagutil.go +++ b/cmd/nerdctl/helpers/flagutil.go @@ -107,6 +107,11 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) if err != nil { return types.GlobalCommandOptions{}, err } + cdiSpecDirs, err := cmd.Flags().GetStringSlice("cdi-spec-dirs") + if err != nil { + return types.GlobalCommandOptions{}, err + } + return types.GlobalCommandOptions{ Debug: debug, DebugFull: debugFull, @@ -123,6 +128,7 @@ func ProcessRootCmdFlags(cmd *cobra.Command) (types.GlobalCommandOptions, error) HostGatewayIP: hostGatewayIP, BridgeIP: bridgeIP, KubeHideDupe: kubeHideDupe, + CDISpecDirs: cdiSpecDirs, }, nil } diff --git a/cmd/nerdctl/helpers/testing_linux.go b/cmd/nerdctl/helpers/testing_linux.go index dd5901177de..bf63686f0c8 100644 --- a/cmd/nerdctl/helpers/testing_linux.go +++ b/cmd/nerdctl/helpers/testing_linux.go @@ -19,7 +19,6 @@ package helpers import ( "fmt" "io" - "net" "os" "os/exec" "path/filepath" @@ -33,64 +32,6 @@ import ( "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) -} - -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 diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index 871d9c97d81..2a051e9e468 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -89,6 +89,12 @@ func convertCommand() *cobra.Command { cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd") // #endregion + // #region soci flags + cmd.Flags().Bool("soci", false, "Convert image to SOCI format. Should be used in conjunction with '--oci'") + cmd.Flags().Int64("soci-min-layer-size", -1, "The minimum size of layers that will be converted to SOCI format") + cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans") + // #endregion + // #region generic flags cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers") cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types") @@ -213,6 +219,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { } // #endregion + // #region soci flags + soci, err := cmd.Flags().GetBool("soci") + if err != nil { + return types.ImageConvertOptions{}, err + } + sociMinLayerSize, err := cmd.Flags().GetInt64("soci-min-layer-size") + if err != nil { + return types.ImageConvertOptions{}, err + } + sociSpanSize, err := cmd.Flags().GetInt64("soci-span-size") + if err != nil { + return types.ImageConvertOptions{}, err + } + // #endregion + // #region generic flags uncompress, err := cmd.Flags().GetBool("uncompress") if err != nil { @@ -268,6 +289,13 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { OverlayFsType: overlaybdFsType, OverlaydbDBStr: overlaybdDbstr, // #endregion + // #region soci flags + Soci: soci, + SociOptions: types.SociOptions{ + SpanSize: sociSpanSize, + MinLayerSize: sociMinLayerSize, + }, + // #endregion // #region generic flags Uncompress: uncompress, Oci: oci, diff --git a/cmd/nerdctl/image/image_convert_linux_test.go b/cmd/nerdctl/image/image_convert_linux_test.go index 3d2615741bc..b26358ec8b9 100644 --- a/cmd/nerdctl/image/image_convert_linux_test.go +++ b/cmd/nerdctl/image/image_convert_linux_test.go @@ -115,16 +115,16 @@ func TestImageConvertNydusVerify(t *testing.T) { 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)) + data.Labels().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)) + helpers.Ensure("tag", data.Identifier("converted-image"), data.Labels().Get(remoteImageKey)) + helpers.Ensure("push", data.Labels().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)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(remoteImageKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { @@ -133,7 +133,7 @@ func TestImageConvertNydusVerify(t *testing.T) { "--source", testutil.CommonImage, "--target", - data.Get(remoteImageKey), + data.Labels().Get(remoteImageKey), "--source-insecure", "--target-insecure", ) diff --git a/cmd/nerdctl/image/image_encrypt_linux_test.go b/cmd/nerdctl/image/image_encrypt_linux_test.go index ac883133f63..40cb742a10c 100644 --- a/cmd/nerdctl/image/image_encrypt_linux_test.go +++ b/cmd/nerdctl/image/image_encrypt_linux_test.go @@ -26,7 +26,6 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" - 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/testregistry" @@ -36,7 +35,6 @@ func TestImageEncryptJWE(t *testing.T) { nerdtest.Setup() var registry *testregistry.RegistryServer - var keyPair *testhelpers.JweKeyPair const remoteImageKey = "remoteImageKey" @@ -50,18 +48,20 @@ func TestImageEncryptJWE(t *testing.T) { 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.Labels().Get(remoteImageKey)) } helpers.Anyhow("rmi", "-f", data.Identifier("decrypted")) }, Setup: func(data test.Data, helpers test.Helpers) { + pri, pub := nerdtest.GenerateJWEKeyPair(data, helpers) + data.Labels().Set("private", pri) + data.Labels().Set("public", pub) + 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) + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+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) @@ -69,13 +69,13 @@ func TestImageEncryptJWE(t *testing.T) { helpers.Ensure("push", encryptImageRef) helpers.Anyhow("rmi", "-f", encryptImageRef) helpers.Anyhow("rmi", "-f", testutil.CommonImage) - data.Set(remoteImageKey, encryptImageRef) + data.Labels().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")) + helpers.Fail("pull", data.Labels().Get(remoteImageKey)) + helpers.Ensure("pull", "--quiet", "--unpack=false", data.Labels().Get(remoteImageKey)) + helpers.Fail("image", "decrypt", "--key="+data.Labels().Get("public"), data.Labels().Get(remoteImageKey), data.Identifier("decrypted")) // decryption needs prv key, not pub key + return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("private"), data.Labels().Get(remoteImageKey), data.Identifier("decrypted")) }, Expected: test.Expects(0, nil, nil), } diff --git a/cmd/nerdctl/image/image_history_test.go b/cmd/nerdctl/image/image_history_test.go index f4f0d2c0505..1281c00fa47 100644 --- a/cmd/nerdctl/image/image_history_test.go +++ b/cmd/nerdctl/image/image_history_test.go @@ -29,6 +29,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/v2/pkg/formatter" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -82,7 +83,7 @@ func TestImageHistory(t *testing.T) { // 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.Write(nerdtest.DataRoot, test.ConfigValue(data.Temp().Path())) helpers.Ensure("pull", "--quiet", "--platform", "linux/arm64", testutil.CommonImage) }, SubTests: []*test.Case{ @@ -93,11 +94,6 @@ func TestImageHistory(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") @@ -108,8 +104,13 @@ func TestImageHistory(t *testing.T) { 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[0].Size, "0B", info) + assert.Equal(t, history[0].CreatedSince, formatter.TimeSinceInHuman(compTime1), info) + assert.Equal(t, history[0].Snapshot, "", info) + assert.Equal(t, history[0].Comment, "", 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].CreatedSince, formatter.TimeSinceInHuman(compTime2), info) assert.Equal(t, history[1].Snapshot, "sha256:56bf55b8eed1f0b4794a30386e4d1d3da949c…", info) assert.Equal(t, history[1].Comment, "", info) }), diff --git a/cmd/nerdctl/image/image_inspect.go b/cmd/nerdctl/image/image_inspect.go index 833b770f9b0..5dd7238a151 100644 --- a/cmd/nerdctl/image/image_inspect.go +++ b/cmd/nerdctl/image/image_inspect.go @@ -21,11 +21,14 @@ import ( "github.com/spf13/cobra" + "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/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" "github.com/containerd/nerdctl/v2/pkg/cmd/image" + "github.com/containerd/nerdctl/v2/pkg/formatter" ) func inspectCommand() *cobra.Command { @@ -102,7 +105,18 @@ func imageInspectAction(cmd *cobra.Command, args []string) error { } defer cancel() - return image.Inspect(ctx, client, args, options) + entries, err := image.Inspect(ctx, client, args, options) + if err != nil { + return err + } + + // Display + if len(entries) > 0 { + if formatErr := formatter.FormatSlice(options.Format, options.Stdout, entries); formatErr != nil { + log.G(ctx).Error(formatErr) + } + } + return err } func imageInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/image/image_list_test.go b/cmd/nerdctl/image/image_list_test.go index 38b0b034fed..96b04c3faa7 100644 --- a/cmd/nerdctl/image/image_list_test.go +++ b/cmd/nerdctl/image/image_list_test.go @@ -148,10 +148,10 @@ 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() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) - data.Set("buildCtx", buildCtx) + data.Labels().Set("buildCtx", buildCtx) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rmi", "-f", "taggedimage:one-fragment-one") @@ -159,8 +159,8 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" 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")) + data.Labels().Set("builtImageID", data.Identifier()) + return helpers.Command("build", "-t", data.Identifier(), data.Labels().Get("buildCtx")) }, Expected: test.Expects(0, nil, nil), SubTests: []*test.Case{ @@ -169,7 +169,7 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" Command: test.Command("images", "--filter", "label=foo=bar"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("builtImageID")), + Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, @@ -178,7 +178,7 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" Command: test.Command("images", "--filter", "label=foo=bar1"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.DoesNotContain(data.Get("builtImageID")), + Output: expect.DoesNotContain(data.Labels().Get("builtImageID")), } }, }, @@ -187,7 +187,7 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" 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: expect.Contains(data.Get("builtImageID")), + Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, @@ -196,7 +196,7 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" 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: expect.DoesNotContain(data.Get("builtImageID")), + Output: expect.DoesNotContain(data.Labels().Get("builtImageID")), } }, }, @@ -205,39 +205,40 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" Command: test.Command("images", "--filter", "label=version"), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("builtImageID")), + Output: expect.Contains(data.Labels().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"))) + return helpers.Command("images", "--filter", fmt.Sprintf("reference=%s*", data.Labels().Get("builtImageID"))) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - Output: expect.Contains(data.Get("builtImageID")), + Output: expect.Contains(data.Labels().Get("builtImageID")), } }, }, { Description: "reference=tagged*:*fragment*", Command: test.Command("images", "--filter", "reference=tagged*:*fragment*"), - Expected: test.Expects(0, nil, expect.All( - expect.Contains("one-"), - expect.Contains("two-"), - )), + Expected: test.Expects( + 0, + nil, + expect.Contains("one-", "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"))) + return helpers.Command("images", "--filter", fmt.Sprintf("before=%s:latest", data.Labels().Get("builtImageID"))) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( expect.Contains(testutil.ImageRepo(testutil.CommonImage)), - expect.DoesNotContain(data.Get("builtImageID")), + expect.DoesNotContain(data.Labels().Get("builtImageID")), ), } }, @@ -248,7 +249,7 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(data.Get("builtImageID")), + expect.Contains(data.Labels().Get("builtImageID")), expect.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), ), } @@ -259,24 +260,22 @@ RUN echo "actually creating a layer so that docker sets the createdAt time" 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: expect.All( - expect.DoesNotContain(data.Get("builtImageID")), - expect.DoesNotContain(testutil.ImageRepo(testutil.CommonImage)), + Output: expect.DoesNotContain( + data.Labels().Get("builtImageID"), + 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), + Expected: test.Expects(expect.ExitCodeGenericFail, []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), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("no such image: ")}, nil), }, }, } @@ -296,17 +295,17 @@ func TestImagesFilterDangling(t *testing.T) { dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-notag-string"] `, testutil.CommonImage) - buildCtx := data.TempDir() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) - data.Set("buildCtx", buildCtx) + data.Labels().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")) + return helpers.Command("build", data.Labels().Get("buildCtx")) }, Expected: test.Expects(0, nil, nil), SubTests: []*test.Case{ diff --git a/cmd/nerdctl/image/image_load_test.go b/cmd/nerdctl/image/image_load_test.go index 3533c183e84..6598ab93db5 100644 --- a/cmd/nerdctl/image/image_load_test.go +++ b/cmd/nerdctl/image/image_load_test.go @@ -43,7 +43,7 @@ func TestLoadStdinFromPipe(t *testing.T) { 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("save", identifier, "-o", filepath.Join(data.Temp().Path(), "common.tar")) helpers.Ensure("rmi", "-f", identifier) }, Cleanup: func(data test.Data, helpers test.Helpers) { @@ -51,9 +51,9 @@ func TestLoadStdinFromPipe(t *testing.T) { }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { cmd := helpers.Command("load") - reader, err := os.Open(filepath.Join(data.TempDir(), "common.tar")) + reader, err := os.Open(filepath.Join(data.Temp().Path(), "common.tar")) assert.NilError(t, err, "failed to open common.tar") - cmd.WithStdin(reader) + cmd.Feed(reader) return cmd }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -94,14 +94,14 @@ func TestLoadQuiet(t *testing.T) { 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("save", identifier, "-o", filepath.Join(data.Temp().Path(), "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")) + return helpers.Command("load", "--quiet", "--input", filepath.Join(data.Temp().Path(), "common.tar")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ diff --git a/cmd/nerdctl/image/image_prune_test.go b/cmd/nerdctl/image/image_prune_test.go index 1c8b402e332..402ea7bb94a 100644 --- a/cmd/nerdctl/image/image_prune_test.go +++ b/cmd/nerdctl/image/image_prune_test.go @@ -71,7 +71,7 @@ func TestImagePrune(t *testing.T) { CMD ["echo", "nerdctl-test-image-prune"] `, testutil.CommonImage) - buildCtx := data.TempDir() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) helpers.Ensure("build", buildCtx) @@ -119,7 +119,7 @@ func TestImagePrune(t *testing.T) { CMD ["echo", "nerdctl-test-image-prune"] `, testutil.CommonImage) - buildCtx := data.TempDir() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) helpers.Ensure("build", buildCtx) @@ -163,7 +163,7 @@ func TestImagePrune(t *testing.T) { CMD ["echo", "nerdctl-test-image-prune-filter-label"] LABEL foo=bar LABEL version=0.1`, testutil.CommonImage) - buildCtx := data.TempDir() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) helpers.Ensure("build", "-t", data.Identifier(), buildCtx) @@ -203,22 +203,22 @@ LABEL version=0.1`, testutil.CommonImage) 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() + buildCtx := data.Temp().Path() 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()) + data.Labels().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: expect.All( - expect.DoesNotContain(data.Get("imageID")), + expect.DoesNotContain(data.Labels().Get("imageID")), func(stdout string, info string, t *testing.T) { imgList := helpers.Capture("images") - assert.Assert(t, strings.Contains(imgList, data.Get("imageID")), info) + assert.Assert(t, strings.Contains(imgList, data.Labels().Get("imageID")), info) }, ), } @@ -234,10 +234,10 @@ CMD ["echo", "nerdctl-test-image-prune-until"]`, testutil.CommonImage) Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(data.Get("imageID")), + expect.Contains(data.Labels().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) + assert.Assert(t, !strings.Contains(imgList, data.Labels().Get("imageID")), imgList, info) }, ), } diff --git a/cmd/nerdctl/image/image_pull_linux_test.go b/cmd/nerdctl/image/image_pull_linux_test.go index 9939bc72529..6dd12b34aba 100644 --- a/cmd/nerdctl/image/image_pull_linux_test.go +++ b/cmd/nerdctl/image/image_pull_linux_test.go @@ -18,8 +18,6 @@ package image import ( "fmt" - "os" - "path/filepath" "strconv" "strings" "testing" @@ -30,17 +28,19 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" - 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/testregistry" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) func TestImagePullWithCosign(t *testing.T) { + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) + nerdtest.Setup() - var registry *testregistry.RegistryServer - var keyPair *testhelpers.CosignKeyPair + var reg *registry.Server testCase := &test.Case{ Require: require.All( @@ -48,45 +48,47 @@ func TestImagePullWithCosign(t *testing.T) { nerdtest.Build, require.Binary("cosign"), require.Not(nerdtest.Docker), + nerdtest.Registry, ), + 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) + data.Temp().Save(dockerfile, "Dockerfile") + pri, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "1") + reg = nerdtest.RegistryWithNoAuth(data, helpers, 0, false) + reg.Setup(data, helpers) + testImageRef := fmt.Sprintf("%s:%d/%s", "127.0.0.1", reg.Port, data.Identifier()) + buildCtx := data.Temp().Path() - 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) + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+pri, testImageRef+":one") + helpers.Ensure("push", "--sign=cosign", "--cosign-key="+pri, testImageRef+":two") + + data.Labels().Set("public_key", pub) + data.Labels().Set("image_ref", 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()) + if reg != nil { + reg.Cleanup(data, helpers) + testImageRef := data.Labels().Get("image_ref") 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", "--quiet", "--verify=cosign", "--cosign-key="+keyPair.PublicKey, data.Get("imageref")+":one") + return helpers.Command( + "pull", "--quiet", "--verify=cosign", + "--cosign-key="+data.Labels().Get("public_key"), + data.Labels().Get("image_ref")+":one") }, Expected: test.Expects(0, nil, nil), }, @@ -96,8 +98,8 @@ CMD ["echo", "nerdctl-build-test-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", "--quiet", "--verify=cosign", "--cosign-key="+newKeyPair.PublicKey, data.Get("imageref")+":two") + _, pub := nerdtest.GenerateCosignKeyPair(data, helpers, "2") + return helpers.Command("pull", "--quiet", "--verify=cosign", "--cosign-key="+pub, data.Labels().Get("image_ref")+":two") }, Expected: test.Expects(12, nil, nil), }, @@ -110,42 +112,44 @@ CMD ["echo", "nerdctl-build-test-string"] func TestImagePullPlainHttpWithDefaultPort(t *testing.T) { nerdtest.Setup() - var registry *testregistry.RegistryServer + var reg *registry.Server + dockerfile := fmt.Sprintf(`FROM %s +CMD ["echo", "nerdctl-build-test-string"] + `, testutil.CommonImage) testCase := &test.Case{ Require: require.All( require.Linux, require.Not(nerdtest.Docker), nerdtest.Build, + nerdtest.Registry, ), + Setup: func(data test.Data, helpers test.Helpers) { - base := testutil.NewBase(t) - registry = testregistry.NewWithNoAuth(base, 80, false) + data.Temp().Save(dockerfile, "Dockerfile") + reg = nerdtest.RegistryWithNoAuth(data, helpers, 80, false) + reg.Setup(data, helpers) 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) + reg.IP.String(), data.Identifier(), strings.Split(testutil.CommonImage, ":")[1]) + buildCtx := data.Temp().Path() - 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) + + data.Labels().Set("image_ref", 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) + return helpers.Command("--insecure-registry", "pull", data.Labels().Get("image_ref")) }, + 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) + if reg != nil { + reg.Cleanup(data, helpers) + helpers.Anyhow("rmi", "-f", data.Labels().Get("image_ref")) } }, } @@ -165,17 +169,21 @@ func TestImagePullSoci(t *testing.T) { // NOTE: these tests cannot be run in parallel, as they depend on the output of host `mount` // They also feel prone to raciness... + NoParallel: true, + SubTests: []*test.Case{ { Description: "Run without specifying SOCI index", NoParallel: true, - Data: test.WithData("remoteSnapshotsExpectedCount", "11"). - Set("sociIndexDigest", ""), + Data: test.WithLabels(map[string]string{ + "remoteSnapshotsExpectedCount": "11", + "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"))) + data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) @@ -188,13 +196,14 @@ func TestImagePullSoci(t *testing.T) { }, 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")) + Output: func(stdout string, _ string, t *testing.T) { + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, - data.Get("remoteSnapshotsExpectedCount"), + data.Labels().Get("remoteSnapshotsExpectedCount"), strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), - info) + "expected remote snapshot count to match", + ) }, } }, @@ -202,13 +211,15 @@ func TestImagePullSoci(t *testing.T) { { Description: "Run with bad SOCI index", NoParallel: true, - Data: test.WithData("remoteSnapshotsExpectedCount", "11"). - Set("sociIndexDigest", "sha256:thisisabadindex0000000000000000000000000000000000000000000000000"), + Data: test.WithLabels(map[string]string{ + "remoteSnapshotsExpectedCount": "11", + "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"))) + data.Labels().Set("remoteSnapshotsInitialCount", strconv.Itoa(strings.Count(stdout, "fuse.rawBridge"))) }, }) helpers.Ensure("--snapshotter=soci", "pull", testutil.FfmpegSociImage) @@ -222,12 +233,12 @@ func TestImagePullSoci(t *testing.T) { 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")) + remoteSnapshotsInitialCount, _ := strconv.Atoi(data.Labels().Get("remoteSnapshotsInitialCount")) remoteSnapshotsActualCount := strings.Count(stdout, "fuse.rawBridge") assert.Equal(t, - data.Get("remoteSnapshotsExpectedCount"), + data.Labels().Get("remoteSnapshotsExpectedCount"), strconv.Itoa(remoteSnapshotsActualCount-remoteSnapshotsInitialCount), - info) + "expected remote snapshot count to match") }, } }, diff --git a/cmd/nerdctl/image/image_push_linux_test.go b/cmd/nerdctl/image/image_push_linux_test.go index 66d26512a8a..bf10f371a23 100644 --- a/cmd/nerdctl/image/image_push_linux_test.go +++ b/cmd/nerdctl/image/image_push_linux_test.go @@ -67,16 +67,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", data.Get("testImageRef")) + return helpers.Command("push", data.Labels().Get("testImageRef")) }, Expected: test.Expects(1, []error{errors.New("server gave HTTP response to HTTPS client")}, nil), }, @@ -87,16 +87,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, @@ -106,11 +106,11 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + return helpers.Command("push", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, @@ -121,16 +121,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, @@ -141,19 +141,19 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, @@ -164,19 +164,19 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Get("testImageRef")) + return helpers.Command("push", "--hosts-dir", registryTokenAuthHTTPSRandom.HostsDir, data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, @@ -187,16 +187,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--insecure-registry", data.Get("testImageRef")) + return helpers.Command("push", "--insecure-registry", data.Labels().Get("testImageRef")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -219,16 +219,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("testImageRef")) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Get("testImageRef")) + return helpers.Command("push", "--insecure-registry", "--allow-nondistributable-artifacts", data.Labels().Get("testImageRef")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -254,16 +254,16 @@ func TestPush(t *testing.T) { 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) + data.Labels().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")) + if data.Labels().Get("testImageRef") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().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")) + return helpers.Command("push", "--snapshotter=soci", "--insecure-registry", "--soci-span-size=2097152", "--soci-min-layer-size=20971520", data.Labels().Get("testImageRef")) }, Expected: test.Expects(0, nil, nil), }, diff --git a/cmd/nerdctl/image/image_remove_test.go b/cmd/nerdctl/image/image_remove_test.go index e91f05c69af..11f2f050636 100644 --- a/cmd/nerdctl/image/image_remove_test.go +++ b/cmd/nerdctl/image/image_remove_test.go @@ -129,11 +129,11 @@ func TestRemove(t *testing.T) { repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8] - data.Set(imgShortIDKey, imgShortID) + data.Labels().Set(imgShortIDKey, imgShortID) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("rmi", "-f", data.Get(imgShortIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(imgShortIDKey)) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -186,11 +186,8 @@ func TestRemove(t *testing.T) { return &test.Expected{ Output: func(stdout string, info string, t *testing.T) { helpers.Command("images").Run(&test.Expected{ - Output: expect.All( - expect.DoesNotContain(repoName), - // a created container with removed image doesn't impact other `rmi` command - expect.DoesNotContain(nginxRepoName), - ), + // a created container with removed image doesn't impact other `rmi` command + Output: expect.DoesNotContain(repoName, nginxRepoName), }) }, } @@ -238,11 +235,11 @@ func TestRemove(t *testing.T) { repoName, _ := imgutil.ParseRepoTag(testutil.CommonImage) imgShortID := strings.TrimPrefix(img.RepoDigests[0], repoName+"@sha256:")[0:8] - data.Set(imgShortIDKey, imgShortID) + data.Labels().Set(imgShortIDKey, imgShortID) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("rm", "-f", data.Identifier()) - helpers.Anyhow("rmi", "-f", data.Get(imgShortIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(imgShortIDKey)) }, Command: test.Command("rmi", "-f", testutil.CommonImage), Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -330,17 +327,17 @@ func TestIssue3016(t *testing.T) { helpers.Ensure("tag", testutil.CommonImage, tagID) - data.Set(tagIDKey, tagID) + data.Labels().Set(tagIDKey, tagID) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("rmi", data.Get(tagIDKey)) + return helpers.Command("rmi", data.Labels().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{ + helpers.Command("images", data.Labels().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) diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 7e61ef6e28f..4f3bf58de6a 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -44,13 +44,13 @@ func TestSaveContent(t *testing.T) { 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) + return helpers.Command("save", "-o", filepath.Join(data.Temp().Path(), "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) + rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) assert.NilError(t, err) etcOSReleasePath := filepath.Join(rootfsPath, "/etc/os-release") etcOSReleaseBytes, err := os.ReadFile(etcOSReleasePath) @@ -83,8 +83,8 @@ func TestSave(t *testing.T) { 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")) + if data.Labels().Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { @@ -97,14 +97,14 @@ func TestSave(t *testing.T) { } else { id = strings.Split(img.RepoDigests[0], ":")[1] } - tarPath := filepath.Join(data.TempDir(), "out.tar") + tarPath := filepath.Join(data.Temp().Path(), "out.tar") helpers.Ensure("save", "-o", tarPath, id) helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("load", "-i", tarPath) - data.Set("id", id) + data.Labels().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") + return helpers.Command("run", "--rm", data.Labels().Get("id"), "sh", "-euxc", "echo foo") }, Expected: test.Expects(0, nil, expect.Equals("foo\n")), }, @@ -112,8 +112,8 @@ func TestSave(t *testing.T) { 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")) + if data.Labels().Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { @@ -126,14 +126,14 @@ func TestSave(t *testing.T) { id = strings.Split(img.RepoDigests[0], ":")[1] } helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) - tarPath := filepath.Join(data.TempDir(), "out.tar") + tarPath := filepath.Join(data.Temp().Path(), "out.tar") helpers.Ensure("save", "-o", tarPath, id) helpers.Ensure("rmi", "-f", testutil.CommonImage) helpers.Ensure("load", "-i", tarPath) - data.Set("id", id) + data.Labels().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") + return helpers.Command("run", "--rm", data.Labels().Get("id"), "sh", "-euxc", "echo foo") }, Expected: test.Expects(0, nil, expect.Equals("foo\n")), }, @@ -161,8 +161,8 @@ func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { 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")) + if data.Labels().Get("id") != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get("id")) } }, Setup: func(data test.Data, helpers test.Helpers) { @@ -175,11 +175,11 @@ func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { id = strings.Split(img.RepoDigests[0], ":")[1] } helpers.Ensure("tag", testutil.CommonImage, data.Identifier()) - tarPath := filepath.Join(data.TempDir(), "out.tar") + tarPath := filepath.Join(data.Temp().Path(), "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) + data.Labels().Set("id", id) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("images", "--no-trunc") @@ -189,7 +189,7 @@ func TestSaveMultipleImagesWithSameIDAndLoad(t *testing.T) { ExitCode: 0, Errors: []error{}, Output: func(stdout string, info string, t *testing.T) { - assert.Equal(t, strings.Count(stdout, data.Get("id")), 2) + assert.Equal(t, strings.Count(stdout, data.Labels().Get("id")), 2) }, } }, diff --git a/cmd/nerdctl/inspect/inspect.go b/cmd/nerdctl/inspect/inspect.go index 53d02858d5a..0473f1bddc3 100644 --- a/cmd/nerdctl/inspect/inspect.go +++ b/cmd/nerdctl/inspect/inspect.go @@ -22,20 +22,23 @@ import ( "github.com/spf13/cobra" + "github.com/containerd/log" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" - containerCmd "github.com/containerd/nerdctl/v2/cmd/nerdctl/container" + 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" + 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/formatter" "github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" ) func Command() *cobra.Command { - var cmd = &cobra.Command{ + cmd := &cobra.Command{ Use: "inspect", Short: "Return low-level information on objects.", Args: cobra.MinimumNArgs(1), @@ -79,6 +82,10 @@ func inspectAction(cmd *cobra.Command, args []string) error { } namespace := globalOptions.Namespace address := globalOptions.Address + format, err := cmd.Flags().GetString("format") + if err != nil { + return err + } inspectType, err := cmd.Flags().GetString("type") if err != nil { return err @@ -117,19 +124,20 @@ func inspectAction(cmd *cobra.Command, args []string) error { var containerInspectOptions types.ContainerInspectOptions if inspectImage { platform := "" - imageInspectOptions, err = imageCmd.InspectOptions(cmd, &platform) + imageInspectOptions, err = imagecmd.InspectOptions(cmd, &platform) if err != nil { return err } } if inspectContainer { - containerInspectOptions, err = containerCmd.InspectOptions(cmd) + containerInspectOptions, err = containercmd.InspectOptions(cmd) if err != nil { return err } } var errs []error + var entries []interface{} for _, req := range args { var ni int var nc int @@ -150,12 +158,16 @@ func inspectAction(cmd *cobra.Command, args []string) error { if ni == 0 && nc == 0 { errs = append(errs, fmt.Errorf("no such object %s", req)) } else if ni > 0 { - if err := image.Inspect(ctx, client, []string{req}, imageInspectOptions); err != nil { + if imageEntries, err := image.Inspect(ctx, client, []string{req}, imageInspectOptions); err != nil { errs = append(errs, err) + } else { + entries = append(entries, imageEntries...) } } else if nc > 0 { - if err := container.Inspect(ctx, client, []string{req}, containerInspectOptions); err != nil { + if containerEntries, err := container.Inspect(ctx, client, []string{req}, containerInspectOptions); err != nil { errs = append(errs, err) + } else { + entries = append(entries, containerEntries...) } } } @@ -164,6 +176,10 @@ func inspectAction(cmd *cobra.Command, args []string) error { return fmt.Errorf("%d errors: %v", len(errs), errs) } + if formatErr := formatter.FormatSlice(format, cmd.OutOrStdout(), entries); formatErr != nil { + log.G(ctx).Error(formatErr) + } + return nil } diff --git a/cmd/nerdctl/inspect/inspect_test.go b/cmd/nerdctl/inspect/inspect_test.go index 18b6d6ffee5..954b0e73eac 100644 --- a/cmd/nerdctl/inspect/inspect_test.go +++ b/cmd/nerdctl/inspect/inspect_test.go @@ -17,11 +17,60 @@ package inspect import ( + "encoding/json" "testing" + "gotest.tools/v3/assert" + + "github.com/containerd/nerdctl/mod/tigron/test" + + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestMain(m *testing.M) { testutil.M(m) } + +func TestInspectSimpleCase(t *testing.T) { + nerdtest.Setup() + testCase := &test.Case{ + Description: "inspect container and image return one single json array", + Setup: func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier() + helpers.Ensure("run", "-d", "--quiet", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + identifier := data.Identifier() + helpers.Anyhow("rm", "-f", identifier) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", testutil.CommonImage, data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + Output: func(stdout string, info string, t *testing.T) { + var inspectResult []json.RawMessage + err := json.Unmarshal([]byte(stdout), &inspectResult) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + assert.Equal(t, len(inspectResult), 2, "Unexpectedly got multiple results\n"+info) + + var dci dockercompat.Image + err = json.Unmarshal(inspectResult[0], &dci) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + inspecti := nerdtest.InspectImage(helpers, testutil.CommonImage) + assert.Equal(t, dci.ID, inspecti.ID, info) + + var dcc dockercompat.Container + err = json.Unmarshal(inspectResult[1], &dcc) + assert.NilError(t, err, "Unable to unmarshal output\n"+info) + inspectc := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Assert(t, dcc.ID == inspectc.ID, info) + }, + } + }, + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go index aa36f35b08e..9a7b09805b5 100644 --- a/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_compose_linux_test.go @@ -19,6 +19,7 @@ package ipfs import ( "fmt" "io" + "os" "strconv" "strings" "testing" @@ -57,7 +58,7 @@ func TestIPFSCompNoBuild(t *testing.T) { // 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)) + data.Labels().Set(ipfsAddrKey, fmt.Sprintf("/ip4/%s/tcp/%d", ipfsRegistry.IP, ipfsRegistry.Port)) // Ensure we have the images helpers.Ensure("pull", "--quiet", testutil.WordpressImage) @@ -113,15 +114,15 @@ func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { 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)) + ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) + ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) + data.Labels().Set(composeExtraKey, "--ipfs-address="+data.Labels().Get(ipfsAddrKey)) } else { ipfsCIDWP = pushToIPFS(helpers, testutil.WordpressImage) ipfsCIDMD = pushToIPFS(helpers, testutil.MariaDBImage) } - data.Set(wordpressImageCIDKey, ipfsCIDWP) - data.Set(mariaImageCIDKey, ipfsCIDMD) + data.Labels().Set(wordpressImageCIDKey, ipfsCIDWP) + data.Labels().Set(mariaImageCIDKey, ipfsCIDMD) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -130,9 +131,9 @@ func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { // 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)) + if data.Labels().Get(mariaImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mariaImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(wordpressImageCIDKey)) } */ } @@ -140,7 +141,7 @@ func subtestTestIPFSCompNoB(t *testing.T, stargz bool, byAddr bool) *test.Case { 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)) + data.Labels().Set("wordpressPort", strconv.Itoa(safePort)) composeUP(data, helpers, fmt.Sprintf(` version: '3.1' @@ -174,7 +175,7 @@ services: volumes: wordpress: db: -`, data.Get(wordpressImageCIDKey), safePort, data.Get(mariaImageCIDKey)), data.Get(composeExtraKey)) +`, data.Labels().Get(wordpressImageCIDKey), safePort, data.Labels().Get(mariaImageCIDKey)), data.Labels().Get(composeExtraKey)) // FIXME: need to break down composeUP into testable commands instead // Right now, this is just a dummy placeholder return helpers.Command("info") @@ -211,13 +212,14 @@ func TestIPFSCompBuild(t *testing.T) { // 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) + // This should not take longer than that + ipfsServer.WithTimeout(30 * time.Second) + ipfsServer.Background() // Apparently necessary to let it start... time.Sleep(time.Second) // Save nginx to ipfs - data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.NginxAlpineImage)) const dockerComposeYAML = ` services: @@ -228,7 +230,7 @@ services: ` dockerfile := fmt.Sprintf(`FROM %s/ipfs/%s COPY index.html /usr/share/nginx/html/index.html -`, listenAddr, data.Get(mainImageCIDKey)) +`, listenAddr, data.Labels().Get(mainImageCIDKey)) comp = testutil.NewComposeDir(t, dockerComposeYAML) comp.WriteFile("Dockerfile", dockerfile) @@ -237,9 +239,8 @@ COPY index.html /usr/share/nginx/html/index.html 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) + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) + ipfsServer.Signal(os.Kill) } if comp != nil { helpers.Anyhow("compose", "-f", comp.YAMLFullPath(), "down", "-v") @@ -289,7 +290,7 @@ func composeUP(data test.Data, helpers test.Helpers, dockerComposeYAML string, o 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) + resp, err := nettestutil.HTTPGet("http://127.0.0.1:"+data.Labels().Get("wordpressPort"), 5, false) if err != nil { return err } diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go index 48068b076c8..de1dc16d239 100644 --- a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -51,7 +51,7 @@ func TestIPFSAddrWithKubo(t *testing.T) { 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) + data.Labels().Set(ipfsAddrKey, ipfsAddr) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -65,17 +65,17 @@ func TestIPFSAddrWithKubo(t *testing.T) { 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", "--quiet", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) - data.Set(mainImageCIDKey, ipfsCID) + ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Labels().Get(ipfsAddrKey))) + helpers.Ensure("pull", "--quiet", "--ipfs-address", data.Labels().Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Labels().Set(mainImageCIDKey, ipfsCID) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(mainImageCIDKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, @@ -88,17 +88,17 @@ func TestIPFSAddrWithKubo(t *testing.T) { 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", "--quiet", "--ipfs-address", data.Get(ipfsAddrKey), "ipfs://"+ipfsCID) - data.Set(mainImageCIDKey, ipfsCID) + ipfsCID := pushToIPFS(helpers, testutil.CommonImage, fmt.Sprintf("--ipfs-address=%s", data.Labels().Get(ipfsAddrKey)), "--estargz") + helpers.Ensure("pull", "--quiet", "--ipfs-address", data.Labels().Get(ipfsAddrKey), "ipfs://"+ipfsCID) + data.Labels().Set(mainImageCIDKey, ipfsCID) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(mainImageCIDKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, diff --git a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go index 20865efc834..5c044bf36af 100644 --- a/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_registry_linux_test.go @@ -70,8 +70,9 @@ func TestIPFSNerdctlRegistry(t *testing.T) { // 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) + // This should not take longer than that + ipfsServer.WithTimeout(30 * time.Second) + ipfsServer.Background() // Apparently necessary to let it start... time.Sleep(time.Second) } @@ -79,7 +80,7 @@ func TestIPFSNerdctlRegistry(t *testing.T) { testCase.Cleanup = func(data test.Data, helpers test.Helpers) { if ipfsServer != nil { // Close the server once done - ipfsServer.Run(nil) + ipfsServer.Signal(os.Kill) } } @@ -88,16 +89,16 @@ func TestIPFSNerdctlRegistry(t *testing.T) { 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)) + data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage)) + helpers.Ensure("pull", "--quiet", data.Labels().Get(ipfsImageURLKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(ipfsImageURLKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(ipfsImageURLKey)) + if data.Labels().Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "echo", "hello") + return helpers.Command("run", "--rm", data.Labels().Get(ipfsImageURLKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, @@ -106,16 +107,16 @@ func TestIPFSNerdctlRegistry(t *testing.T) { 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)) + data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage, "--estargz")) + helpers.Ensure("pull", "--quiet", data.Labels().Get(ipfsImageURLKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(ipfsImageURLKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(ipfsImageURLKey)) + if data.Labels().Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") + return helpers.Command("run", "--rm", data.Labels().Get(ipfsImageURLKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, @@ -125,18 +126,18 @@ func TestIPFSNerdctlRegistry(t *testing.T) { 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)) + if data.Labels().Get(ipfsImageURLKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(ipfsImageURLKey)) } }, Setup: func(data test.Data, helpers test.Helpers) { - data.Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage)) + data.Labels().Set(ipfsImageURLKey, listenAddr+"/ipfs/"+pushToIPFS(helpers, testutil.CommonImage)) dockerfile := fmt.Sprintf(`FROM %s CMD ["echo", "nerdctl-build-test-string"] - `, data.Get(ipfsImageURLKey)) + `, data.Labels().Get(ipfsImageURLKey)) - buildCtx := data.TempDir() + buildCtx := data.Temp().Path() err := os.WriteFile(filepath.Join(buildCtx, "Dockerfile"), []byte(dockerfile), 0o600) assert.NilError(helpers.T(), err) diff --git a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go index dda1771cadd..9edd24f5af1 100644 --- a/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_simple_linux_test.go @@ -24,7 +24,6 @@ import ( "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" - testhelpers "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -53,16 +52,16 @@ func TestIPFSSimple(t *testing.T) { Description: "with default snapshotter", NoParallel: true, Setup: func(data test.Data, helpers test.Helpers) { - data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) - helpers.Ensure("pull", "--quiet", "ipfs://"+data.Get(mainImageCIDKey)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(mainImageCIDKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "echo", "hello") + return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "echo", "hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), }, @@ -74,16 +73,16 @@ func TestIPFSSimple(t *testing.T) { 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", "--quiet", "ipfs://"+data.Get(mainImageCIDKey)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz")) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().Get(mainImageCIDKey)) }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get(mainImageCIDKey) != "" { - helpers.Anyhow("rmi", "-f", data.Get(mainImageCIDKey)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") + return helpers.Command("run", "--rm", data.Labels().Get(mainImageCIDKey), "ls", "/.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("sha256:.*[.]json[\n]"))), }, @@ -91,32 +90,32 @@ func TestIPFSSimple(t *testing.T) { 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", "--quiet", "ipfs://"+data.Get(mainImageCIDKey)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().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("run", "--name", data.Identifier("commit-container"), data.Labels().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"))) + data.Labels().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", "--quiet", "ipfs://"+data.Get(transformedImageCIDKey)) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().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)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) } }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "--rm", data.Get(transformedImageCIDKey), "cat", "/hello") + return helpers.Command("run", "--rm", data.Labels().Get(transformedImageCIDKey), "cat", "/hello") }, Expected: test.Expects(0, nil, expect.Equals("hello\n")), @@ -129,32 +128,32 @@ func TestIPFSSimple(t *testing.T) { 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", "--quiet", "ipfs://"+data.Get(mainImageCIDKey)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage, "--estargz")) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().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("run", "--name", data.Identifier("commit-container"), data.Labels().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"))) + data.Labels().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", "--quiet", "ipfs://"+data.Get(transformedImageCIDKey)) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().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)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().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") + return helpers.Command("run", "--rm", data.Labels().Get(transformedImageCIDKey), "sh", "-c", "--", "cat /hello && ls /.stargz-snapshotter") }, Expected: test.Expects(0, nil, expect.Match(regexp.MustCompile("hello[\n]sha256:.*[.]json[\n]"))), @@ -164,18 +163,16 @@ func TestIPFSSimple(t *testing.T) { NoParallel: true, Require: require.Binary("openssl"), Setup: func(data test.Data, helpers test.Helpers) { - data.Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) - helpers.Ensure("pull", "--quiet", "ipfs://"+data.Get(mainImageCIDKey)) + data.Labels().Set(mainImageCIDKey, pushToIPFS(helpers, testutil.CommonImage)) + helpers.Ensure("pull", "--quiet", "ipfs://"+data.Labels().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) + pri, pub := nerdtest.GenerateJWEKeyPair(data, helpers) + data.Labels().Set("prv", pri) + data.Labels().Set("pub", pub) // Encrypt the image, and verify it is encrypted - helpers.Ensure("image", "encrypt", "--recipient=jwe:"+keyPair.Pub, data.Get(mainImageCIDKey), data.Identifier("encrypted")) + helpers.Ensure("image", "encrypt", "--recipient=jwe:"+pub, data.Labels().Get(mainImageCIDKey), data.Identifier("encrypted")) cmd := helpers.Command("image", "inspect", "--mode=native", "--format={{len .Index.Manifests}}", data.Identifier("encrypted")) cmd.Run(&test.Expected{ Output: expect.Equals("1\n"), @@ -186,19 +183,19 @@ func TestIPFSSimple(t *testing.T) { }) // Push the encrypted image and save the CID - data.Set(transformedImageCIDKey, pushToIPFS(helpers, data.Identifier("encrypted"))) + data.Labels().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)) + helpers.Ensure("rmi", "-f", data.Labels().Get(mainImageCIDKey)) + helpers.Ensure("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) // Pull back without unpacking - helpers.Ensure("pull", "--quiet", "--unpack=false", "ipfs://"+data.Get(transformedImageCIDKey)) + helpers.Ensure("pull", "--quiet", "--unpack=false", "ipfs://"+data.Labels().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)) + if data.Labels().Get(mainImageCIDKey) != "" { + helpers.Anyhow("rmi", "-f", data.Labels().Get(mainImageCIDKey)) + helpers.Anyhow("rmi", "-f", data.Labels().Get(transformedImageCIDKey)) } }, SubTests: []*test.Case{ @@ -208,7 +205,7 @@ func TestIPFSSimple(t *testing.T) { 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")) + return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("pub"), data.Labels().Get(transformedImageCIDKey), data.Identifier("decrypted")) }, Expected: test.Expects(1, nil, nil), }, @@ -218,7 +215,7 @@ func TestIPFSSimple(t *testing.T) { 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")) + return helpers.Command("image", "decrypt", "--key="+data.Labels().Get("prv"), data.Labels().Get(transformedImageCIDKey), data.Identifier("decrypted")) }, Expected: test.Expects(0, nil, nil), }, diff --git a/cmd/nerdctl/issues/main_linux_test.go b/cmd/nerdctl/issues/main_linux_test.go index 4703897a7e2..12f1b9384d6 100644 --- a/cmd/nerdctl/issues/main_linux_test.go +++ b/cmd/nerdctl/issues/main_linux_test.go @@ -39,7 +39,7 @@ func TestIssue108(t *testing.T) { { 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 := helpers.Command("run", "--quiet", "-it", "--rm", "--net=host", testutil.CommonImage, "echo", "this was always working") cmd.WithPseudoTTY() return cmd }, @@ -48,7 +48,7 @@ func TestIssue108(t *testing.T) { { 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 := helpers.Command("run", "--quiet", "--rm", "--net=host", "-it", testutil.CommonImage, "echo", "this was not working due to issue #108") cmd.WithPseudoTTY() return cmd }, diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 3eab9293db5..55544b33ad8 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -33,6 +33,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/imgutil/dockerconfigresolver" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" "github.com/containerd/nerdctl/v2/pkg/testutil/testca" "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) @@ -76,7 +77,7 @@ func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { ag.configPath, _ = os.MkdirTemp(base.T.TempDir(), "docker-config") } args := []string{"login"} - if base.Target == "nerdctl" { + if !nerdtest.IsDocker() { args = append(args, "--debug-full") } args = append(args, ag.args...) diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index 8bdcad92381..88d753a170c 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -186,11 +186,12 @@ func initRootCmdFlags(rootCmd *cobra.Command, tomlPath string) (*pflag.FlagSet, 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") + rootCmd.PersistentFlags().StringSlice("cdi-spec-dirs", cfg.CDISpecDirs, "The directories to search for CDI spec files. Defaults to /etc/cdi,/var/run/cdi") + rootCmd.PersistentFlags().String("userns-remap", cfg.UsernsRemap, "Support idmapping for creating and running containers. This options is only supported on linux. If `host` is passed, no idmapping is done. if a user name is passed, it does idmapping based on the uidmap and gidmap ranges specified in /etc/subuid and /etc/subgid respectively") return aliasToBeInherited, nil } func newApp() (*cobra.Command, error) { - tomlPath := ncdefaults.NerdctlTOML() if v, ok := os.LookupEnv("NERDCTL_TOML"); ok { tomlPath = v @@ -217,6 +218,10 @@ Config file ($NERDCTL_TOML): %s return nil, err } + if err := resetSavedSETUID(); err != nil { + return nil, err + } + rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { globalOptions, err := helpers.ProcessRootCmdFlags(cmd) if err != nil { @@ -243,12 +248,12 @@ Config file ($NERDCTL_TOML): %s } // 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. + // valid, safe path segments. // 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 { + if err = store.IsFilesystemSafe(globalOptions.Namespace); err != nil { return err } if appNeedsRootlessParentMain(cmd, args) { diff --git a/cmd/nerdctl/main_linux.go b/cmd/nerdctl/main_linux.go index 0bf52d3ca23..5aba7c2f480 100644 --- a/cmd/nerdctl/main_linux.go +++ b/cmd/nerdctl/main_linux.go @@ -18,6 +18,7 @@ package main import ( "github.com/spf13/cobra" + "golang.org/x/sys/unix" "github.com/containerd/nerdctl/v2/cmd/nerdctl/apparmor" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" @@ -66,3 +67,18 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { func addApparmorCommand(rootCmd *cobra.Command) { rootCmd.AddCommand(apparmor.Command()) } + +// resetSavedSETUID drops the saved UID of a setuid-root process to the original real UID. +// This ensures the process cannot regain root privileges later. +// It only performs the operation if the process is currently running with effective UID 0 (root) +// and was started by a non-root user (i.e., real UID != effective UID). +// For more info see issue https://github.com/containerd/nerdctl/issues/4098 +func resetSavedSETUID() error { + var err error + uid := unix.Getuid() + euid := unix.Geteuid() + if uid != euid && euid == 0 { + err = unix.Setresuid(0, 0, uid) + } + return err +} diff --git a/cmd/nerdctl/main_freebsd.go b/cmd/nerdctl/main_nolinux.go similarity index 91% rename from cmd/nerdctl/main_freebsd.go rename to cmd/nerdctl/main_nolinux.go index 391d34cfeed..fb0f1230943 100644 --- a/cmd/nerdctl/main_freebsd.go +++ b/cmd/nerdctl/main_nolinux.go @@ -1,3 +1,5 @@ +//go:build !linux + /* Copyright The containerd Authors. @@ -27,3 +29,8 @@ func appNeedsRootlessParentMain(cmd *cobra.Command, args []string) bool { func addApparmorCommand(rootCmd *cobra.Command) { // NOP } + +func resetSavedSETUID() error { + // NOP + return nil +} diff --git a/cmd/nerdctl/main_test_test.go b/cmd/nerdctl/main_test_test.go index 36c6a96e3a9..4a06ea3667f 100644 --- a/cmd/nerdctl/main_test_test.go +++ b/cmd/nerdctl/main_test_test.go @@ -61,7 +61,7 @@ func TestTest(t *testing.T) { { Description: "failure with multiple error testing", Command: test.Command("-fail"), - Expected: test.Expects(-1, []error{errors.New("unknown"), errors.New("shorthand")}, nil), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errors.New("unknown"), errors.New("shorthand")}, nil), }, { Description: "success with exact output testing", @@ -72,29 +72,29 @@ func TestTest(t *testing.T) { }, { Description: "data propagation", - Data: test.WithData("status", "uninitialized"), + Data: test.WithLabels(map[string]string{"status": "uninitialized"}), Setup: func(data test.Data, helpers test.Helpers) { - data.Set("status", data.Get("status")+"-setup") + data.Labels().Set("status", data.Labels().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") + cmd := helpers.Custom("printf", data.Labels().Get("status")) + data.Labels().Set("status", data.Labels().Get("status")+"-command") return cmd }, Cleanup: func(data test.Data, helpers test.Helpers) { - if data.Get("status") == "uninitialized" { + if data.Labels().Get("status") == "uninitialized" { return } - if data.Get("status") != "uninitialized-setup-command" { - log.Fatalf("unexpected status label %q", data.Get("status")) + if data.Labels().Get("status") != "uninitialized-setup-command" { + log.Fatalf("unexpected status label %q", data.Labels().Get("status")) } - data.Set("status", data.Get("status")+"-cleanup") + data.Labels().Set("status", data.Labels().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")) + return helpers.Custom("printf", data.Labels().Get("status")) }, Expected: test.Expects(0, nil, expect.Equals("uninitialized-setup-command")), }, diff --git a/cmd/nerdctl/network/network_create_linux_test.go b/cmd/nerdctl/network/network_create_linux_test.go index 418f07fde74..01ed943467f 100644 --- a/cmd/nerdctl/network/network_create_linux_test.go +++ b/cmd/nerdctl/network/network_create_linux_test.go @@ -17,6 +17,7 @@ package network import ( + "fmt" "net" "strings" "testing" @@ -26,7 +27,6 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/test" - ipv6helper "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -42,7 +42,7 @@ func TestNetworkCreate(t *testing.T) { 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) + data.Labels().Set("subnet", netw.IPAM.Config[0].Subnet) helpers.Ensure("network", "create", data.Identifier("1")) }, @@ -51,7 +51,7 @@ func TestNetworkCreate(t *testing.T) { 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")) + data.Labels().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 { @@ -59,8 +59,8 @@ func TestNetworkCreate(t *testing.T) { 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) + assert.Assert(t, strings.Contains(stdout, data.Labels().Get("subnet")), info) + assert.Assert(t, !strings.Contains(data.Labels().Get("container2"), data.Labels().Get("subnet")), info) }, } }, @@ -83,7 +83,7 @@ func TestNetworkCreate(t *testing.T) { Require: nerdtest.OnlyIPv6, Setup: func(data test.Data, helpers test.Helpers) { subnetStr := "2001:db8:8::/64" - data.Set("subnetStr", subnetStr) + data.Labels().Set("subnetStr", subnetStr) _, _, err := net.ParseCIDR(subnetStr) assert.Assert(t, err == nil) @@ -99,9 +99,9 @@ func TestNetworkCreate(t *testing.T) { 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) + _, subnet, _ := net.ParseCIDR(data.Labels().Get("subnetStr")) + ip := nerdtest.FindIPv6(stdout) + assert.Assert(t, subnet.Contains(ip), fmt.Sprintf("subnet %s contains ip %s", subnet, ip)) }, } }, diff --git a/cmd/nerdctl/network/network_inspect.go b/cmd/nerdctl/network/network_inspect.go index adc1a96507d..c61fe893eca 100644 --- a/cmd/nerdctl/network/network_inspect.go +++ b/cmd/nerdctl/network/network_inspect.go @@ -22,6 +22,7 @@ import ( "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" ) @@ -59,13 +60,22 @@ func inspectAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - return network.Inspect(cmd.Context(), types.NetworkInspectOptions{ + + options := types.NetworkInspectOptions{ GOptions: globalOptions, Mode: mode, Format: format, Networks: args, Stdout: cmd.OutOrStdout(), - }) + } + + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return network.Inspect(ctx, client, options) } func networkInspectShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index cb89f25f2d4..ed4bb00d1e5 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -19,6 +19,7 @@ package network import ( "encoding/json" "errors" + "os/exec" "strings" "testing" @@ -29,6 +30,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -43,7 +45,7 @@ func TestNetworkInspect(t *testing.T) { testCase.Setup = func(data test.Data, helpers test.Helpers) { helpers.Ensure("network", "create", data.Identifier("basenet")) - data.Set("basenet", data.Identifier("basenet")) + data.Labels().Set("basenet", data.Identifier("basenet")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -132,7 +134,7 @@ func TestNetworkInspect(t *testing.T) { 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 }}")) + id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -142,7 +144,7 @@ func TestNetworkInspect(t *testing.T) { 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")) + assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } }, @@ -153,7 +155,7 @@ func TestNetworkInspect(t *testing.T) { // 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: require.Not(require.Windows), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Get("basenet"), "--format", "{{ .Id }}")) + id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) return helpers.Command("network", "inspect", id[0:25]) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { @@ -163,7 +165,7 @@ func TestNetworkInspect(t *testing.T) { 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")) + assert.Equal(t, dc[0].Name, data.Labels().Get("basenet")) }, } }, @@ -174,15 +176,15 @@ func TestNetworkInspect(t *testing.T) { // 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: require.Not(require.Windows), Setup: func(data test.Data, helpers test.Helpers) { - id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Get("basenet"), "--format", "{{ .Id }}")) + id := strings.TrimSpace(helpers.Capture("network", "inspect", data.Labels().Get("basenet"), "--format", "{{ .Id }}")) helpers.Ensure("network", "create", id[0:12]) - data.Set("netname", id[0:12]) + data.Labels().Set("netname", id[0:12]) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("network", "remove", data.Get("netname")) + helpers.Anyhow("network", "remove", data.Labels().Get("netname")) }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "inspect", data.Get("netname")) + return helpers.Command("network", "inspect", data.Labels().Get("netname")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -191,7 +193,7 @@ func TestNetworkInspect(t *testing.T) { 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")) + assert.Equal(t, dc[0].Name, data.Labels().Get("netname")) }, } }, @@ -247,7 +249,11 @@ func TestNetworkInspect(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - cmd := helpers.Custom("nerdctl", "--namespace", data.Identifier()) + // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need + // to retrieve the binary name. + // Note that we know this works already, so no need to assert err. + bin, _ := exec.LookPath(testutil.GetTarget()) + cmd := helpers.Custom(bin, "--namespace", data.Identifier()) com := cmd.Clone() com.WithArgs("network", "inspect", data.Identifier()) @@ -278,6 +284,42 @@ func TestNetworkInspect(t *testing.T) { } }, }, + { + Description: "Verify that only active containers appear in the network inspect output", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("network", "create", data.Identifier("nginx-network-1")) + helpers.Ensure("network", "create", data.Identifier("nginx-network-2")) + helpers.Ensure("create", "--name", data.Identifier("nginx-container-1"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) + helpers.Ensure("create", "--name", data.Identifier("nginx-container-2"), "--network", data.Identifier("nginx-network-1"), testutil.NginxAlpineImage) + helpers.Ensure("create", "--name", data.Identifier("nginx-container-on-diff-network"), "--network", data.Identifier("nginx-network-2"), testutil.NginxAlpineImage) + helpers.Ensure("start", data.Identifier("nginx-container-1"), data.Identifier("nginx-container-on-diff-network")) + data.Labels().Set("nginx-container-1-id", strings.Trim(helpers.Capture("inspect", data.Identifier("nginx-container-1"), "--format", "{{.Id}}"), "\n")) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-1")) + helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-2")) + helpers.Anyhow("rm", "-f", data.Identifier("nginx-container-on-diff-network")) + helpers.Anyhow("network", "remove", data.Identifier("nginx-network-1")) + helpers.Anyhow("network", "remove", data.Identifier("nginx-network-2")) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("network", "inspect", data.Identifier("nginx-network-1")) + }, + 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.Identifier("nginx-network-1")) + // Assert only the "running" containers on the same network are returned. + assert.Equal(t, 1, len(dc[0].Containers), "Expected a single container as per configuration, but got multiple.") + assert.Equal(t, data.Identifier("nginx-container-1"), dc[0].Containers[data.Labels().Get("nginx-container-1-id")].Name) + }, + } + }, + }, } testCase.Run(t) diff --git a/cmd/nerdctl/network/network_list_linux_test.go b/cmd/nerdctl/network/network_list_linux_test.go index 96f35dedf2e..3bf6f9e912f 100644 --- a/cmd/nerdctl/network/network_list_linux_test.go +++ b/cmd/nerdctl/network/network_list_linux_test.go @@ -31,12 +31,12 @@ 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"))) + data.Labels().Set("identifier", data.Identifier()) + data.Labels().Set("label", "mylabel=label-1") + data.Labels().Set("net1", data.Identifier("1")) + data.Labels().Set("net2", data.Identifier("2")) + data.Labels().Set("netID1", helpers.Capture("network", "create", "--label="+data.Labels().Get("label"), data.Labels().Get("net1"))) + data.Labels().Set("netID2", helpers.Capture("network", "create", data.Labels().Get("net2"))) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -48,7 +48,7 @@ func TestNetworkLsFilter(t *testing.T) { { Description: "filter label", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Get("label")) + return helpers.Command("network", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -56,7 +56,7 @@ func TestNetworkLsFilter(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]: {}, + data.Labels().Get("netID1")[:12]: {}, } for _, name := range lines { @@ -70,7 +70,7 @@ func TestNetworkLsFilter(t *testing.T) { { Description: "filter name", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Get("net2")) + return helpers.Command("network", "ls", "--quiet", "--filter", "name="+data.Labels().Get("net2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -78,7 +78,7 @@ func TestNetworkLsFilter(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]: {}, + data.Labels().Get("netID2")[:12]: {}, } for _, name := range lines { diff --git a/cmd/nerdctl/network/network_remove_linux_test.go b/cmd/nerdctl/network/network_remove_linux_test.go index 48906cbad08..7a86ec37962 100644 --- a/cmd/nerdctl/network/network_remove_linux_test.go +++ b/cmd/nerdctl/network/network_remove_linux_test.go @@ -40,11 +40,11 @@ func TestNetworkRemove(t *testing.T) { Setup: func(data test.Data, helpers test.Helpers) { identifier := data.Identifier() helpers.Ensure("network", "create", identifier) - data.Set("netID", nerdtest.InspectNetwork(helpers, identifier).ID) + data.Labels().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") + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("network", "rm", data.Identifier()) @@ -56,7 +56,7 @@ func TestNetworkRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found", info) }, } @@ -81,14 +81,14 @@ func TestNetworkRemove(t *testing.T) { 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) + data.Labels().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") + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "rm", data.Get("netID")) + return helpers.Command("network", "rm", data.Labels().Get("netID")) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) @@ -97,7 +97,7 @@ func TestNetworkRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found", info) }, } @@ -107,14 +107,14 @@ func TestNetworkRemove(t *testing.T) { 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) + data.Labels().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") + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) + assert.NilError(t, err, "failed to find network br-"+data.Labels().Get("netID")[:12], "%v") }, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("network", "rm", data.Get("netID")[:12]) + return helpers.Command("network", "rm", data.Labels().Get("netID")[:12]) }, Cleanup: func(data test.Data, helpers test.Helpers) { helpers.Anyhow("network", "rm", data.Identifier()) @@ -123,7 +123,7 @@ func TestNetworkRemove(t *testing.T) { return &test.Expected{ ExitCode: 0, Output: func(stdout string, info string, t *testing.T) { - _, err := netlink.LinkByName("br-" + data.Get("netID")[:12]) + _, err := netlink.LinkByName("br-" + data.Labels().Get("netID")[:12]) assert.Error(t, err, "Link not found", info) }, } diff --git a/cmd/nerdctl/system/system_events_linux_test.go b/cmd/nerdctl/system/system_events_linux_test.go index 20f1f8e3f06..431abdafb0b 100644 --- a/cmd/nerdctl/system/system_events_linux_test.go +++ b/cmd/nerdctl/system/system_events_linux_test.go @@ -29,8 +29,11 @@ import ( ) 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("pull", testutil.CommonImage) + cmd := helpers.Command("events", "--filter", data.Labels().Get("filter"), "--format", "json") + // 3 seconds is too short on slow rig (EL8) + cmd.WithTimeout(10 * time.Second) + cmd.Background() helpers.Ensure("run", "--rm", testutil.CommonImage) return cmd } @@ -46,11 +49,13 @@ func TestEventFilters(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, - Output: expect.Contains(data.Get("output")), + Output: expect.Contains(data.Labels().Get("output")), } }, - Data: test.WithData("filter", "event=START"). - Set("output", "\"Status\":\"start\""), + Data: test.WithLabels(map[string]string{ + "filter": "event=START", + "output": "\"Status\":\"start\"", + }), }, { Description: "StartEventFilter", @@ -58,11 +63,13 @@ func TestEventFilters(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, - Output: expect.Contains(data.Get("output")), + Output: expect.Contains(data.Labels().Get("output")), } }, - Data: test.WithData("filter", "event=start"). - Set("output", "tatus\":\"start\""), + Data: test.WithLabels(map[string]string{ + "filter": "event=start", + "output": "tatus\":\"start\"", + }), }, { Description: "UnsupportedEventFilter", @@ -71,11 +78,13 @@ func TestEventFilters(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, - Output: expect.Contains(data.Get("output")), + Output: expect.Contains(data.Labels().Get("output")), } }, - Data: test.WithData("filter", "event=unknown"). - Set("output", "\"Status\":\"unknown\""), + Data: test.WithLabels(map[string]string{ + "filter": "event=unknown", + "output": "\"Status\":\"unknown\"", + }), }, { Description: "StatusFilter", @@ -83,11 +92,13 @@ func TestEventFilters(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, - Output: expect.Contains(data.Get("output")), + Output: expect.Contains(data.Labels().Get("output")), } }, - Data: test.WithData("filter", "status=start"). - Set("output", "tatus\":\"start\""), + Data: test.WithLabels(map[string]string{ + "filter": "status=start", + "output": "tatus\":\"start\"", + }), }, { Description: "UnsupportedStatusFilter", @@ -96,11 +107,13 @@ func TestEventFilters(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: expect.ExitCodeTimeout, - Output: expect.Contains(data.Get("output")), + Output: expect.Contains(data.Labels().Get("output")), } }, - Data: test.WithData("filter", "status=unknown"). - Set("output", "\"Status\":\"unknown\""), + Data: test.WithLabels(map[string]string{ + "filter": "status=unknown", + "output": "\"Status\":\"unknown\"", + }), }, } diff --git a/cmd/nerdctl/system/system_info_test.go b/cmd/nerdctl/system/system_info_test.go index 6248a1e0711..8c4bfe10041 100644 --- a/cmd/nerdctl/system/system_info_test.go +++ b/cmd/nerdctl/system/system_info_test.go @@ -19,6 +19,7 @@ package system import ( "encoding/json" "fmt" + "os/exec" "testing" "gotest.tools/v3/assert" @@ -29,6 +30,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/infoutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) @@ -43,6 +45,11 @@ func testInfoComparator(stdout string, info string, t *testing.T) { func TestInfo(t *testing.T) { testCase := nerdtest.Setup() + // Note: some functions need to be tested without the automatic --namespace nerdctl-test argument, so we need + // to retrieve the binary name. + // Note that we know this works already, so no need to assert err. + bin, _ := exec.LookPath(testutil.GetTarget()) + testCase.SubTests = []*test.Case{ { Description: "info", @@ -58,7 +65,7 @@ func TestInfo(t *testing.T) { Description: "info with namespace", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("nerdctl", "info") + return helpers.Custom(bin, "info") }, Expected: test.Expects(0, nil, expect.Contains("Namespace: default")), }, @@ -69,7 +76,7 @@ func TestInfo(t *testing.T) { }, Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Custom("nerdctl", "info") + return helpers.Custom(bin, "info") }, Expected: test.Expects(0, nil, expect.Contains("Namespace: test")), }, diff --git a/cmd/nerdctl/system/system_prune_linux_test.go b/cmd/nerdctl/system/system_prune_linux_test.go index 45a0b896d01..70a4a9df651 100644 --- a/cmd/nerdctl/system/system_prune_linux_test.go +++ b/cmd/nerdctl/system/system_prune_linux_test.go @@ -48,12 +48,12 @@ func TestSystemPrune(t *testing.T) { helpers.Ensure("run", "-v", fmt.Sprintf("%s:/volume", data.Identifier()), "--net", data.Identifier(), "--name", data.Identifier(), testutil.CommonImage) - data.Set("anonIdentifier", anonIdentifier) + data.Labels().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("volume", "rm", data.Labels().Get("anonIdentifier")) helpers.Anyhow("rm", "-f", data.Identifier()) }, Command: test.Command("system", "prune", "-f", "--volumes", "--all"), @@ -66,7 +66,7 @@ func TestSystemPrune(t *testing.T) { 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(volumes, data.Labels().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) diff --git a/cmd/nerdctl/volume/volume_create_test.go b/cmd/nerdctl/volume/volume_create_test.go index 8e761c6e015..8eedd781751 100644 --- a/cmd/nerdctl/volume/volume_create_test.go +++ b/cmd/nerdctl/volume/volume_create_test.go @@ -85,7 +85,7 @@ func TestVolumeCreate(t *testing.T) { helpers.Anyhow("volume", "rm", "-f", data.Identifier()) }, // NOTE: docker returns 125 on this - Expected: test.Expects(-1, []error{errdefs.ErrInvalidArgument}, nil), + Expected: test.Expects(expect.ExitCodeGenericFail, []error{errdefs.ErrInvalidArgument}, nil), }, { Description: "creating already existing volume should succeed", diff --git a/cmd/nerdctl/volume/volume_inspect_test.go b/cmd/nerdctl/volume/volume_inspect_test.go index 7e627a119b5..b42b3d41558 100644 --- a/cmd/nerdctl/volume/volume_inspect_test.go +++ b/cmd/nerdctl/volume/volume_inspect_test.go @@ -18,7 +18,6 @@ package volume import ( "crypto/rand" - "encoding/json" "errors" "fmt" "os" @@ -31,6 +30,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/expect" "github.com/containerd/nerdctl/mod/tigron/require" "github.com/containerd/nerdctl/mod/tigron/test" + "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -65,8 +65,8 @@ func TestVolumeInspect(t *testing.T) { 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")) + data.Labels().Set("vol1", data.Identifier("first")) + data.Labels().Set("vol2", data.Identifier("second")) } testCase.Cleanup = func(data test.Data, helpers test.Helpers) { @@ -93,21 +93,17 @@ func TestVolumeInspect(t *testing.T) { { Description: "success", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("volume", "inspect", data.Get("vol1")) + return helpers.Command("volume", "inspect", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.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) - } + expect.Contains(data.Labels().Get("vol1")), + expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { 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].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().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) - }, + }), ), } }, @@ -115,22 +111,18 @@ func TestVolumeInspect(t *testing.T) { { Description: "inspect labels", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("volume", "inspect", data.Get("vol2")) + return helpers.Command("volume", "inspect", data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.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) - } + expect.Contains(data.Labels().Get("vol2")), + expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { 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"])) - }, + }), ), } }, @@ -139,19 +131,15 @@ func TestVolumeInspect(t *testing.T) { Description: "inspect size", Require: require.Not(nerdtest.Docker), Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("volume", "inspect", "--size", data.Get("vol1")) + return helpers.Command("volume", "inspect", "--size", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.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) - } + expect.Contains(data.Labels().Get("vol1")), + expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { assert.Assert(t, dc[0].Size == size, fmt.Sprintf("expected size to be %d (was %d)", size, dc[0].Size)) - }, + }), ), } }, @@ -159,22 +147,17 @@ func TestVolumeInspect(t *testing.T) { { Description: "multi success", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("volume", "inspect", data.Get("vol1"), data.Get("vol2")) + return helpers.Command("volume", "inspect", data.Labels().Get("vol1"), data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.Contains(data.Get("vol1")), - expect.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) - } + expect.Contains(data.Labels().Get("vol1"), data.Labels().Get("vol2")), + expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { 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)) - }, + assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) + assert.Assert(t, dc[1].Name == data.Labels().Get("vol2"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol2"), dc[1].Name)) + }), ), } }, @@ -182,22 +165,18 @@ func TestVolumeInspect(t *testing.T) { { Description: "part success multi", Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Get("vol1")) + return helpers.Command("volume", "inspect", "invalid∞", "nonexistent", data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ ExitCode: 1, Errors: []error{errdefs.ErrNotFound, errdefs.ErrInvalidArgument}, Output: expect.All( - expect.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) - } + expect.Contains(data.Labels().Get("vol1")), + expect.JSON([]native.Volume{}, func(dc []native.Volume, info string, t tig.T) { 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)) - }, + assert.Assert(t, dc[0].Name == data.Labels().Get("vol1"), fmt.Sprintf("expected name to be %q (was %q)", data.Labels().Get("vol1"), dc[0].Name)) + }), ), } }, diff --git a/cmd/nerdctl/volume/volume_list_test.go b/cmd/nerdctl/volume/volume_list_test.go index 0610925df32..8dca19fa584 100644 --- a/cmd/nerdctl/volume/volume_list_test.go +++ b/cmd/nerdctl/volume/volume_list_test.go @@ -122,22 +122,22 @@ func TestVolumeLsFilter(t *testing.T) { 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) + data.Labels().Set("vol1", vol1) + data.Labels().Set("vol2", vol2) + data.Labels().Set("vol3", vol3) + data.Labels().Set("vol4", vol4) + data.Labels().Set("mainlabel", "mylabel") + data.Labels().Set("label1", label1) + data.Labels().Set("label2", label2) + data.Labels().Set("label3", label3) + data.Labels().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")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol1")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol2")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol3")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("vol4")) } testCase.SubTests = []*test.Case{ { @@ -149,10 +149,10 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, + data.Labels().Get("vol2"): {}, + data.Labels().Get("vol3"): {}, + data.Labels().Get("vol4"): {}, } var numMatches = 0 for _, name := range lines { @@ -170,7 +170,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -178,9 +178,9 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, + data.Labels().Get("vol2"): {}, + data.Labels().Get("vol3"): {}, } for _, name := range lines { _, ok := volNames[name] @@ -193,7 +193,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -201,7 +201,7 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] @@ -214,7 +214,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")+"=") + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel")+"=") }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -227,7 +227,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("label1"), "--filter", "label="+data.Labels().Get("label2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -240,7 +240,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "label="+data.Labels().Get("mainlabel"), "--filter", "label="+data.Labels().Get("label4")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -248,8 +248,8 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, + data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] @@ -262,7 +262,7 @@ func TestVolumeLsFilter(t *testing.T) { { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Labels().Get("vol1")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -270,7 +270,7 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, } for _, name := range lines { _, ok := volNames[name] @@ -282,10 +282,8 @@ func TestVolumeLsFilter(t *testing.T) { }, { 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")) + return helpers.Command("volume", "ls", "--quiet", "--filter", "name="+data.Labels().Get("vol1"), "--filter", "name="+data.Labels().Get("vol2")) }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ @@ -293,8 +291,8 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, + data.Labels().Get("vol2"): {}, } for _, name := range lines { _, ok := volNames[name] @@ -316,8 +314,8 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol2"): {}, + data.Labels().Get("vol4"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) @@ -347,8 +345,8 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol2"): {}, + data.Labels().Get("vol4"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) @@ -378,8 +376,8 @@ func TestVolumeLsFilter(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"): {}, + data.Labels().Get("vol1"): {}, + data.Labels().Get("vol3"): {}, } var tab = tabutil.NewReader("VOLUME NAME\tDIRECTORY\tSIZE") var err = tab.ParseHeader(lines[0]) diff --git a/cmd/nerdctl/volume/volume_namespace_test.go b/cmd/nerdctl/volume/volume_namespace_test.go index a4b351d9286..341d2d37204 100644 --- a/cmd/nerdctl/volume/volume_namespace_test.go +++ b/cmd/nerdctl/volume/volume_namespace_test.go @@ -35,14 +35,14 @@ func TestVolumeNamespace(t *testing.T) { // 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()) + data.Labels().Set("root_namespace", data.Identifier()) + data.Labels().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") != "" { + if data.Labels().Get("root_namespace") != "" { helpers.Anyhow("--namespace", data.Identifier(), "volume", "remove", data.Identifier()) helpers.Anyhow("namespace", "remove", data.Identifier()) } @@ -52,7 +52,7 @@ func TestVolumeNamespace(t *testing.T) { { 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")) + return helpers.Command("volume", "inspect", data.Labels().Get("root_volume")) }, Expected: test.Expects(1, []error{ errdefs.ErrNotFound, @@ -61,7 +61,7 @@ func TestVolumeNamespace(t *testing.T) { { 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")) + return helpers.Command("volume", "remove", data.Labels().Get("root_volume")) }, Expected: test.Expects(1, []error{ errdefs.ErrNotFound, @@ -75,9 +75,9 @@ func TestVolumeNamespace(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.DoesNotContain(data.Get("root_volume")), + expect.DoesNotContain(data.Labels().Get("root_volume")), func(stdout string, info string, t *testing.T) { - helpers.Ensure("--namespace", data.Get("root_namespace"), "volume", "inspect", data.Get("root_volume")) + helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, ), } @@ -87,17 +87,17 @@ func TestVolumeNamespace(t *testing.T) { 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")) + return helpers.Command("volume", "create", data.Labels().Get("root_volume")) }, Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("volume", "rm", data.Get("root_volume")) + helpers.Anyhow("volume", "rm", data.Labels().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")) + helpers.Ensure("volume", "inspect", data.Labels().Get("root_volume")) + helpers.Ensure("volume", "rm", data.Labels().Get("root_volume")) + helpers.Ensure("--namespace", data.Labels().Get("root_namespace"), "volume", "inspect", data.Labels().Get("root_volume")) }, } }, diff --git a/cmd/nerdctl/volume/volume_prune_linux_test.go b/cmd/nerdctl/volume/volume_prune_linux_test.go index 10b4e6b85f6..6565f578733 100644 --- a/cmd/nerdctl/volume/volume_prune_linux_test.go +++ b/cmd/nerdctl/volume/volume_prune_linux_test.go @@ -41,18 +41,18 @@ func TestVolumePrune(t *testing.T) { "-v", namedBusy+":/namedbusyvolume", "-v", anonIDBusy+":/anonbusyvolume", testutil.CommonImage) - data.Set("anonIDBusy", anonIDBusy) - data.Set("anonIDDangling", anonIDDangling) - data.Set("namedBusy", namedBusy) - data.Set("namedDangling", namedDangling) + data.Labels().Set("anonIDBusy", anonIDBusy) + data.Labels().Set("anonIDDangling", anonIDDangling) + data.Labels().Set("namedBusy", namedBusy) + data.Labels().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")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("anonIDBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("anonIDDangling")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("namedBusy")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().Get("namedDangling")) } testCase := nerdtest.Setup() @@ -69,15 +69,17 @@ func TestVolumePrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.DoesNotContain(data.Get("anonIDBusy")), - expect.Contains(data.Get("anonIDDangling")), - expect.DoesNotContain(data.Get("namedBusy")), - expect.DoesNotContain(data.Get("namedDangling")), + expect.Contains(data.Labels().Get("anonIDDangling")), + expect.DoesNotContain( + data.Labels().Get("anonIDBusy"), + data.Labels().Get("namedBusy"), + data.Labels().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")) + helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) + helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) + helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) + helpers.Ensure("volume", "inspect", data.Labels().Get("namedDangling")) }, ), } @@ -92,15 +94,13 @@ func TestVolumePrune(t *testing.T) { Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ Output: expect.All( - expect.DoesNotContain(data.Get("anonIDBusy")), - expect.Contains(data.Get("anonIDDangling")), - expect.DoesNotContain(data.Get("namedBusy")), - expect.Contains(data.Get("namedDangling")), + expect.DoesNotContain(data.Labels().Get("anonIDBusy"), data.Labels().Get("namedBusy")), + expect.Contains(data.Labels().Get("anonIDDangling"), data.Labels().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")) + helpers.Ensure("volume", "inspect", data.Labels().Get("anonIDBusy")) + helpers.Fail("volume", "inspect", data.Labels().Get("anonIDDangling")) + helpers.Ensure("volume", "inspect", data.Labels().Get("namedBusy")) + helpers.Fail("volume", "inspect", data.Labels().Get("namedDangling")) }, ), } diff --git a/cmd/nerdctl/volume/volume_remove_linux_test.go b/cmd/nerdctl/volume/volume_remove_linux_test.go index 01680bd30ec..1fb98e77846 100644 --- a/cmd/nerdctl/volume/volume_remove_linux_test.go +++ b/cmd/nerdctl/volume/volume_remove_linux_test.go @@ -89,17 +89,17 @@ func TestVolumeRemove(t *testing.T) { } } assert.Assert(t, anonName != "", "Failed to find anonymous volume id", inspect) - data.Set("anonName", anonName) + data.Labels().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")) + helpers.Anyhow("volume", "rm", "-f", data.Labels().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")) + return helpers.Command("volume", "rm", data.Labels().Get("anonName")) }, Expected: test.Expects(1, []error{errdefs.ErrFailedPrecondition}, nil), diff --git a/docs/command-reference.md b/docs/command-reference.md index 41fcd969fd6..8d61e721c68 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -203,6 +203,8 @@ Resource flags: - :whale: `--cpu-shares`: CPU shares (relative weight) - :whale: `--cpuset-cpus`: CPUs in which to allow execution (0-3, 0,1) - :whale: `--cpuset-mems`: Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems +- :whale: `--cpu-rt-period`: Limit CPU real-time period in microseconds. Only supported with cgroup v1. +- :whale: `--cpu-rt-runtime`: Limit CPU real-time runtime in microseconds. Only supported with cgroup v1. - :whale: `--memory`: Memory limit - :whale: `--memory-reservation`: Memory soft limit - :whale: `--memory-swap`: Swap limit equal to memory plus swap: '-1' to enable unlimited swap @@ -213,6 +215,11 @@ Resource flags: - :whale: `--pids-limit`: Tune container pids limit - :nerd_face: `--cgroup-conf`: Configure cgroup v2 (key=value) - :whale: `--blkio-weight`: Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0) +- :whale: `--blkio-weight-device`: Block IO weight (relative device weight) +- :whale: `--device-read-bps`: Limit read rate (bytes per second) from a device +- :whale: `--device-read-iops`: Limit read rate (IO per second) from a device +- :whale: `--device-write-bps`: Limit write rate (bytes per second) to a device +- :whale: `--device-write-iops`: Limit write rate (IO per second) to a device - :whale: `--cgroupns=(host|private)`: Cgroup namespace to use - Default: "private" on cgroup v2 hosts, "host" on cgroup v1 hosts - :whale: `--cgroup-parent`: Optional parent cgroup for the container @@ -228,6 +235,8 @@ User flags: - :nerd_face: `--umask`: Set the umask inside the container. Defaults to 0022. Corresponds to Podman CLI. - :whale: `--group-add`: Add additional groups to join +- :whale: `--userns`: Set it to `host` to disable user namespacing set in nerdctl.toml or in cli. + Security flags: @@ -235,6 +244,7 @@ Security flags: - :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 +- :whale: `--security-opt writable-cgroups`: making the cgroups writeable - :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 @@ -321,8 +331,12 @@ Logging flags: - :nerd_face: `--log-opt=log-path=`: The log path where the logs are written. The path will be created if it does not exist. If the log file exists, the old file will be renamed to `.1`. - Default: `////-json.log` - Example: `/var/lib/nerdctl/1935db59/containers/default//-json.log` + - :whale: `--log-opt labels=production_status,geo`: A comma-separated list of logging-related labels this daemon accepts. + - :whale: `--log-opt env=os,customer`: A comma-separated list of logging-related environment variables this daemon accepts. - :whale: `--log-driver=journald`: Writes log messages to `journald`. The `journald` daemon must be running on the host machine. - :whale: `--log-opt=tag=