diff --git a/.dockerignore b/.dockerignore index e892fca64ae..4a674952409 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,6 +5,3 @@ _output # golangci-lint /build - -# vagrant -/.vagrant diff --git a/.github/workflows/ghcr-image-build-and-publish.yml b/.github/workflows/ghcr-image-build-and-publish.yml index 0cbd280c1d9..8e554fd72d8 100644 --- a/.github/workflows/ghcr-image-build-and-publish.yml +++ b/.github/workflows/ghcr-image-build-and-publish.yml @@ -31,13 +31,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 diff --git a/.github/workflows/job-build.yml b/.github/workflows/job-build.yml index 7969ef16c7c..0040f9cd89d 100644 --- a/.github/workflows/job-build.yml +++ b/.github/workflows/job-build.yml @@ -35,7 +35,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-go.yml b/.github/workflows/job-lint-go.yml index dbe6c08cf83..4ba1d420c3b 100644 --- a/.github/workflows/job-lint-go.yml +++ b/.github/workflows/job-lint-go.yml @@ -39,7 +39,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-other.yml b/.github/workflows/job-lint-other.yml index 5ebcfb1a24c..b754a2482b7 100644 --- a/.github/workflows/job-lint-other.yml +++ b/.github/workflows/job-lint-other.yml @@ -25,7 +25,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-lint-project.yml b/.github/workflows/job-lint-project.yml index eeea1a363f7..fb4d849db3e 100644 --- a/.github/workflows/job-lint-project.yml +++ b/.github/workflows/job-lint-project.yml @@ -30,7 +30,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 100 path: src/github.com/containerd/nerdctl diff --git a/.github/workflows/job-test-dependencies.yml b/.github/workflows/job-test-dependencies.yml index 4c269d8d01c..c100b9446b5 100644 --- a/.github/workflows/job-test-dependencies.yml +++ b/.github/workflows/job-test-dependencies.yml @@ -31,7 +31,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-container.yml b/.github/workflows/job-test-in-container.yml index b4626d83e67..76789ef09c7 100644 --- a/.github/workflows/job-test-in-container.yml +++ b/.github/workflows/job-test-in-container.yml @@ -67,7 +67,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-host.yml b/.github/workflows/job-test-in-host.yml index 6de3830ae00..09430928c1a 100644 --- a/.github/workflows/job-test-in-host.yml +++ b/.github/workflows/job-test-in-host.yml @@ -83,7 +83,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-lima-freebsd.yml b/.github/workflows/job-test-in-lima-freebsd.yml new file mode 100644 index 00000000000..a7d53d64c8d --- /dev/null +++ b/.github/workflows/job-test-in-lima-freebsd.yml @@ -0,0 +1,65 @@ +name: job-test-in-lima-freebsd + +on: + workflow_call: + inputs: + timeout: + required: true + type: number + runner: + required: true + type: string + +jobs: + test: + name: "FreeBSD" + timeout-minutes: ${{ inputs.timeout }} + runs-on: "${{ inputs.runner }}" + steps: + - name: "Init: checkout" + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 1 + persist-credentials: false + + - name: "Init: lima" + uses: lima-vm/lima-actions/setup@55627e31b78637bf254a8b2a14da8ea7d12564e5 # v1.1.0 + id: lima-actions-setup + + - name: "Init: Cache" + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.cache/lima + key: lima-${{ steps.lima-actions-setup.outputs.version }}-freebsd + + - name: "Init: Install xorriso (required by Lima for running FreeBSD)" + run: | + set -eux + sudo apt-get update + sudo apt-get install -y xorriso + + - name: "Init: start the guest VM" + run: | + set -eux + limactl start --plain --name=default template://freebsd + lima freebsd-version -kru + lima sudo pkg install -y bash go containerd runj + + - name: "Init: copy source into the guest VM" + run: | + set -eux + limactl copy -r . default:/tmp/nerdctl + + - name: "Init: build nerdctl" + run: lima --workdir /tmp/nerdctl sudo go build -o /usr/local/bin/nerdctl ./cmd/nerdctl + + - name: "Run: test-unit" + run: lima --workdir /tmp/nerdctl go test -v ./pkg/... + + - name: "Run: test-integration" + timeout-minutes: 3 + run: | + set -eux + lima sudo containerd >containerd.log 2>&1 & + sleep 3 + lima sudo /usr/local/bin/nerdctl run --rm --net=none dougrabson/freebsd-minimal:13 echo 'Nerdctl is up and running.' diff --git a/.github/workflows/job-test-in-lima.yml b/.github/workflows/job-test-in-lima.yml index c339f738611..3eec402053b 100644 --- a/.github/workflows/job-test-in-lima.yml +++ b/.github/workflows/job-test-in-lima.yml @@ -31,7 +31,7 @@ jobs: GUEST: ${{ inputs.guest }} steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/job-test-in-vagrant.yml b/.github/workflows/job-test-in-vagrant.yml deleted file mode 100644 index cfb9c1feef0..00000000000 --- a/.github/workflows/job-test-in-vagrant.yml +++ /dev/null @@ -1,61 +0,0 @@ -# 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - persist-credentials: false - - - name: "Init: setup cache" - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - 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 index 6bea385bdc5..7e444244616 100644 --- a/.github/workflows/job-test-unit.yml +++ b/.github/workflows/job-test-unit.yml @@ -46,7 +46,7 @@ jobs: steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 879319812c2..1913f7f3ed3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,12 +23,12 @@ jobs: id-token: write # for provenances attestations: write # for provenances steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false # FIXME: setup-qemu-action is depended by `gomodjail pack` - name: "Set up QEMU" - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 - name: "Install go" uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: diff --git a/.github/workflows/workflow-flaky.yml b/.github/workflows/workflow-flaky.yml index ab750f61212..5d507c9d281 100644 --- a/.github/workflows/workflow-flaky.yml +++ b/.github/workflows/workflow-flaky.yml @@ -36,7 +36,7 @@ jobs: test-integration-freebsd: name: "FreeBSD" - uses: ./.github/workflows/job-test-in-vagrant.yml + uses: ./.github/workflows/job-test-in-lima-freebsd.yml with: timeout: 15 runner: ubuntu-24.04 @@ -49,7 +49,7 @@ jobs: ROOTFUL: true steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index decb53fe2c0..cae92db27d3 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -33,9 +33,10 @@ jobs: - runner: ubuntu-24.04 goos: windows # Additionally lint for canary - - runner: ubuntu-24.04 - goos: linux - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # goos: linux + # canary: true with: timeout: 10 go-version: "1.26" @@ -72,8 +73,9 @@ jobs: include: - go-version: "1.26" # Additionally build for canary - - go-version: "1.26" - canary: true + # FIXME: failing since the release of go1.27rc1 + # - go-version: "1.26" + # canary: true with: timeout: 10 go-version: ${{ matrix.go-version }} @@ -85,7 +87,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: "Init: checkout" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 1 persist-credentials: false diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 8afed875f1c..2afcd149afb 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -26,8 +26,9 @@ jobs: - runner: "ubuntu-24.04" - runner: "macos-15" - runner: "windows-2025" - - runner: "ubuntu-24.04" - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: "ubuntu-24.04" + # canary: true with: runner: ${{ matrix.runner }} canary: ${{ matrix.canary && true || false }} @@ -51,7 +52,7 @@ jobs: - runner: ubuntu-24.04-arm # Additionally build for old containerd on amd - runner: ubuntu-24.04 - containerd-version: v1.7.30 + containerd-version: v1.7.33 with: runner: ${{ matrix.runner }} containerd-version: ${{ matrix.containerd-version }} @@ -80,7 +81,7 @@ jobs: # old containerd + old ubuntu + old rootlesskit - runner: ubuntu-22.04 target: rootless - containerd-version: v1.7.30 + containerd-version: v1.7.33 rootlesskit-version: v1.1.1 # gomodjail - runner: ubuntu-24.04 @@ -97,16 +98,17 @@ jobs: # old containerd + old ubuntu - runner: ubuntu-22.04 target: rootful - containerd-version: v1.7.30 + containerd-version: v1.7.33 # ipv6 - runner: ubuntu-24.04 target: rootful ipv6: true skip-flaky: true # all canary - - runner: ubuntu-24.04 - target: rootful - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # target: rootful + # canary: true with: timeout: 80 @@ -128,8 +130,9 @@ jobs: include: # Test on windows w/o canary - runner: windows-2022 - - runner: windows-2025 - canary: true + # FIXME: failing since the release of go1.27rc1 + # - runner: windows-2025 + # canary: true # Test docker on linux - runner: ubuntu-24.04 binary: docker @@ -156,13 +159,13 @@ jobs: # Windows CI still requires containerd v2.2. # [v2.3.0 regression] The virtual machine or container JSON document is invalid. (Hyper-V container) # https://github.com/containerd/containerd/issues/13254 - windows-containerd-version: 2.2.3 - windows-containerd-sha: 81314dd5e3baad958acae0e4d1ff21eb27b7c8f8809232ab06c9f397cd221e02 - linux-containerd-version: 2.3.1 + windows-containerd-version: 2.2.5 + windows-containerd-sha: 8724c3a873b4984f5ee092c8f15c1a98ebbb0f968106cf8f5849ea100f3a0236 + linux-containerd-version: 2.3.2 # FIXME: containerd SHAs are not verified for authenticity (only affects tests) # https://github.com/containerd/nerdctl/issues/4666 # Note: these are for amd64 - linux-containerd-sha: 628448bd973610c656c1cbea8e88b32fafd85b23cc1aa4a3372eb7198478c054 + linux-containerd-sha: 75625e6f6595bb95f3fb9c8123a60534af4a8d9b52d7617065967bcefe71a17a linux-containerd-service-sha: 1941362cbaa89dd591b99c32b050d82c583d3cd2e5fa63085d7017457ec5fca8 linux-cni-version: v1.9.1 linux-cni-sha: b98f74a0f8522f0a83867178729c1aa70f2158f90c45a2ca8fa791db1c76b303 diff --git a/.github/workflows/workflow-tigron.yml b/.github/workflows/workflow-tigron.yml index d9aec7baaf3..2c8024ef658 100644 --- a/.github/workflows/workflow-tigron.yml +++ b/.github/workflows/workflow-tigron.yml @@ -31,11 +31,12 @@ jobs: - runner: windows-2022 - runner: ubuntu-24.04 goos: freebsd - - runner: ubuntu-24.04 - canary: go-canary + # FIXME: failing since the release of go1.27rc1 + # - runner: ubuntu-24.04 + # canary: go-canary steps: - name: "Checkout project" - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 100 persist-credentials: false diff --git a/.gitignore b/.gitignore index 1078655195f..4a674952409 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,3 @@ _output # golangci-lint /build - -# vagrant -/.vagrant -Vagrantfile diff --git a/Dockerfile b/Dockerfile index 70250e01afc..244c8590452 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,22 +17,22 @@ # Basic deps # @BINARY: the binary checksums are verified via Dockerfile.d/SHA256SUMS.d/- -ARG CONTAINERD_VERSION=v2.3.1@64b425cf570b3b8dd1d4cc46da7c1fce65c6651a -ARG RUNC_VERSION=v1.4.2@c241c0bb5e60a8e8c1b2e53d4eca8d0068d8d57e +ARG CONTAINERD_VERSION=v2.3.2@fff62f14765df376e5fc36f5a8f8e795b5670f61 +ARG RUNC_VERSION=v1.4.3@bb14dabeb7185bb72c8c86735d090dcb20f36587 ARG CNI_PLUGINS_VERSION=v1.9.1@BINARY # Extra deps: Build -ARG BUILDKIT_VERSION=v0.30.0@BINARY +ARG BUILDKIT_VERSION=v0.31.0@BINARY # Extra deps: Lazy-pulling ARG STARGZ_SNAPSHOTTER_VERSION=v0.18.2@BINARY # Extra deps: Encryption ARG IMGCRYPT_VERSION=v2.0.2@6892f4df2405cd15acbefd1dca970f53ba38bfda # Extra deps: Rootless -ARG ROOTLESSKIT_VERSION=v3.0.0@BINARY +ARG ROOTLESSKIT_VERSION=v3.0.1@BINARY # Extra deps: bypass4netns ARG BYPASS4NETNS_VERSION=v0.4.2@aa04bd3dcc48c6dae6d7327ba219bda8fe2a4634 # Extra deps: FUSE-OverlayFS -ARG FUSE_OVERLAYFS_VERSION=v1.16@BINARY +ARG FUSE_OVERLAYFS_VERSION=v1.17@BINARY ARG CONTAINERD_FUSE_OVERLAYFS_VERSION=v2.1.7@BINARY # Extra deps: Init ARG TINI_VERSION=v0.19.0@BINARY @@ -47,9 +47,9 @@ ARG GO_VERSION=1.26 ARG UBUNTU_VERSION=24.04 ARG CONTAINERIZED_SYSTEMD_VERSION=v0.1.1 ARG GOTESTSUM_VERSION=v1.13.0 -ARG NYDUS_VERSION=v2.4.1 -ARG SOCI_SNAPSHOTTER_VERSION=0.13.0 -ARG KUBO_VERSION=v0.41.0 +ARG NYDUS_VERSION=v2.4.3 +ARG SOCI_SNAPSHOTTER_VERSION=0.14.1 +ARG KUBO_VERSION=v0.42.0 FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 deleted file mode 100644 index 0beafcc769b..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.30.0 +++ /dev/null @@ -1,2 +0,0 @@ -2da148c50540409988c837e62b72176e4a9ad7855548a2dd6ea1cd0c0d2d360d buildkit-v0.30.0.linux-amd64.tar.gz -d0c9601e49f441dcc5c6493c884275a5470dcc9c188c96573c4dd503f2fde97d buildkit-v0.30.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 new file mode 100644 index 00000000000..2ce0cd2cb67 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/buildkit-v0.31.0 @@ -0,0 +1,2 @@ +a13e961a4e4e1afbece1ddfd6818ff726fe4577a99b2763e677456016827f4b7 buildkit-v0.31.0.linux-amd64.tar.gz +74efb4326bac95ecc562a139d34833f4ae7a8df884780910c7ef2cb50e92db66 buildkit-v0.31.0.linux-arm64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 deleted file mode 100644 index edf43283f18..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.16 +++ /dev/null @@ -1,6 +0,0 @@ -6c9ee54166fe7d33ebbfb085812585441f22ebe2a24a868d0a878d3127bcb89e fuse-overlayfs-aarch64 -fc2a73ace8eb6a0553204532de615d782cb98d86deeb0fa7b5d14347d0b95823 fuse-overlayfs-armv7l -3c07b76b432a5b4e5e0ccd986919b478d096701178617175b0c71bcce7c6f6a0 fuse-overlayfs-ppc64le -404fd7a762255d554e70849612fb6979639e1eb23a740487dbe3bac2bccc37c1 fuse-overlayfs-riscv64 -9e96cfe091b4342b8de3e239a96d5fecfb8692fbb4a201c256790c270526fd1b fuse-overlayfs-s390x -30c6b9e192600d6854e13397974c709d7cabd980b7d1a4d67defd8eb69677e91 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 new file mode 100644 index 00000000000..16001445004 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/fuse-overlayfs-v1.17 @@ -0,0 +1,6 @@ +34c9995c929dd52f45cca985858d7e58d9a9626104bc2610db218aaa11115c23 fuse-overlayfs-aarch64 +1611b906052e22bcdbcb4ede5d208823c175a41368c589fb6fb3cb58d4b32b2b fuse-overlayfs-armv7l +9481de8b724e53a2a7f582f4b8bac59240231cda9fcc6131b05e76cafb95b06a fuse-overlayfs-ppc64le +5821eed68e1aed7ca2c510c2f714f95bfcefb87c49d3f703cd18754e5cb23a3c fuse-overlayfs-riscv64 +a5b991b3edb080ce4160370e3f5b1dd424ffdc4d6e0d8bb0c4a42adfa2fb5f8c fuse-overlayfs-s390x +1684ef18c337702a0378a4e9942802770c83b11aed6a93c445d43e641a1f3c90 fuse-overlayfs-x86_64 diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 deleted file mode 100644 index c3a4d72a3db..00000000000 --- a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.0 +++ /dev/null @@ -1,6 +0,0 @@ -9a6ca1f21c5a21be7738d5cd4cbd287b59fe0c76424750295e10405ca18f0ed5 rootlesskit-aarch64.tar.gz -925ff9f281f8658376ce0647e0e1703806fbc0c05d15a01408e080692583125b rootlesskit-armv7l.tar.gz -d4e8b82fdf104ab1e7bba3059d572b46323ff1da1adcd95cbf9f47a09ed3eb5d rootlesskit-ppc64le.tar.gz -5209498ab7c9446a0bcc8ad6b5e77796696da0dede815ef535017fb8412f99ba rootlesskit-riscv64.tar.gz -6ded9f92668c7838935a85fff51c664747f55343e279471d8871dfa793e7cbed rootlesskit-s390x.tar.gz -9e9e65f11b0a75ffe78f82284fa84528519b94c6c5032a33e6c80ec1924ef8d1 rootlesskit-x86_64.tar.gz diff --git a/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 new file mode 100644 index 00000000000..97504505bb6 --- /dev/null +++ b/Dockerfile.d/SHA256SUMS.d/rootlesskit-v3.0.1 @@ -0,0 +1,6 @@ +fdd9d2aa12bb8914081dfe1cd129c5a6a06cf1f80c732713f905a92c07b9b45c rootlesskit-aarch64.tar.gz +9fafaf5bcbee74e86dc4c4b98c70e936fb224dbb0309e436f467d93218dda4d5 rootlesskit-armv7l.tar.gz +320bab519443a6c353f11e3e3c5e59875a00094acdcc040af239e0730f510fb2 rootlesskit-ppc64le.tar.gz +82e843a9b312f6b89fa5b0bd6b07ed2d32177f8f08950ba4c0eab376183a00de rootlesskit-riscv64.tar.gz +738982e4ad56e8c2e6c4a2958bbd7485b5ecb8fa27900cafc78d8b38da04eb77 rootlesskit-s390x.tar.gz +0850aa446151dfbdca15ed228ff0151751792cb5a99260b9a6738e1b490cc37b rootlesskit-x86_64.tar.gz diff --git a/README.md b/README.md index 3b00cf2ec30..90b92ff807e 100644 --- a/README.md +++ b/README.md @@ -159,15 +159,23 @@ $ limactl start $ lima nerdctl run -d --name nginx -p 127.0.0.1:8080:80 nginx:alpine ``` -### FreeBSD +### Windows -See [`./docs/freebsd.md`](docs/freebsd.md). +Install with [Scoop](https://scoop.sh): -### Windows +``` +scoop install nerdctl +``` + +Regarding compatibility, note that: - Linux containers: Known to work on WSL2 - Windows containers: experimental support for Windows (see below for features that are currently known to work) +### FreeBSD + +See [`./docs/freebsd.md`](docs/freebsd.md). + ### Docker To run containerd and nerdctl inside Docker: diff --git a/Vagrantfile.freebsd b/Vagrantfile.freebsd deleted file mode 100644 index a1928268038..00000000000 --- a/Vagrantfile.freebsd +++ /dev/null @@ -1,70 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -# Copyright The containerd Authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Vagrantfile for FreeBSD -Vagrant.configure("2") do |config| - config.vm.box = "generic/freebsd14" - - memory = 2048 - cpus = 1 - config.vm.provider :virtualbox do |v, o| - v.memory = memory - v.cpus = cpus - end - config.vm.provider :libvirt do |v| - v.memory = memory - v.cpus = cpus - end - - config.vm.synced_folder ".", "/vagrant", type: "rsync" - - config.vm.provision "install", type: "shell", run: "once" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - 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 - cd /vagrant - go install ./cmd/nerdctl - SHELL - end - - config.vm.provision "test-unit", type: "shell", run: "never" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - cd /vagrant - go test -v ./pkg/... - SHELL - end - - config.vm.provision "test-integration", type: "shell", run: "never" do |sh| - sh.inline = <<~SHELL - #!/usr/bin/env bash - set -eux -o pipefail - daemon -o containerd.out containerd - sleep 3 - 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 - -end diff --git a/cmd/nerdctl/compose/compose_run_linux_test.go b/cmd/nerdctl/compose/compose_run_linux_test.go index c68d9fa258d..217c21708d0 100644 --- a/cmd/nerdctl/compose/compose_run_linux_test.go +++ b/cmd/nerdctl/compose/compose_run_linux_test.go @@ -19,15 +19,16 @@ package compose import ( "fmt" "io" + "path/filepath" "strings" "testing" "time" "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/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" "github.com/containerd/nerdctl/v2/pkg/testutil" @@ -68,7 +69,7 @@ services: cmd.WithPseudoTTY() return cmd }, - Expected: test.Expects(0, nil, expect.Contains(expectedOutput)), + Expected: test.Expects(expect.ExitCodeSuccess, 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") @@ -233,10 +234,7 @@ services: } func TestComposeRunWithEnv(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 partialOutput = "bar" dockerComposeYAML := fmt.Sprintf(` services: @@ -248,26 +246,42 @@ services: - "echo $$FOO" `, 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").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "bar" - // 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", "-e", "FOO=bar", "--name", containerName, "alpine").AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "-e", + "FOO=bar", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.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 TestComposeRunWithUser(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 partialOutput = "5000" dockerComposeYAML := fmt.Sprintf(` services: @@ -278,25 +292,86 @@ services: - -u `, 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").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "5000" - // 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", "--user", "5000", "--name", containerName, "alpine").AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--user", + "5000", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.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 TestComposeRunWithLabel(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) +func TestComposeRunWithWorkdir(t *testing.T) { + const expectedOutput = "/tmp" + + dockerComposeYAML := fmt.Sprintf(` +services: + alpine: + image: %s + entrypoint: + - pwd +`, testutil.CommonImage) + + testCase := nerdtest.Setup() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--workdir", + "/tmp", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(expectedOutput)) + + testCase.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 TestComposeRunWithLabel(t *testing.T) { dockerComposeYAML := fmt.Sprintf(` services: alpine: @@ -308,31 +383,54 @@ services: - "foo=bar" `, 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").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - // 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", "--label", "foo=rab", "--label", "x=y", "--name", containerName, "alpine").AssertOK() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--label", + "foo=rab", + "--label", + "x=y", + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + assert.Assert(tt, container.Config != nil, "cannot fetch container config") + assert.Equal(tt, container.Config.Labels["foo"], "rab") + assert.Equal(tt, container.Config.Labels["x"], "y") + }, + } + } - container := base.InspectContainer(containerName) - if container.Config == nil { - log.L.Errorf("test failed, cannot fetch container config") - t.Fail() + testCase.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") } - assert.Equal(t, container.Config.Labels["foo"], "rab") - assert.Equal(t, container.Config.Labels["x"], "y") + + testCase.Run(t) } func TestComposeRunWithArgs(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + const partialOutput = "hello world" dockerComposeYAML := fmt.Sprintf(` services: @@ -342,26 +440,41 @@ services: - echo `, 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").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "hello world" - // 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", partialOutput).AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.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", + partialOutput, + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.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 TestComposeRunWithEntrypoint(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 partialOutput = "hello world" dockerComposeYAML := fmt.Sprintf(` services: @@ -371,25 +484,42 @@ services: - stty # should be changed `, 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").Run() + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - const partialOutput = "hello world" - // 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", "--entrypoint", "echo", "--name", containerName, "alpine", partialOutput).AssertOutContains(partialOutput) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--entrypoint", + "echo", + "--name", + data.Identifier(), + "alpine", + partialOutput, + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(partialOutput)) + + testCase.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 TestComposeRunWithVolume(t *testing.T) { - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - dockerComposeYAML := fmt.Sprintf(` services: alpine: @@ -398,30 +528,53 @@ services: - stty # no meaning, just put any command `, 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").Run() + const destinationDir = "/data" - // The directory is automatically removed by Cleanup - tmpDir := t.TempDir() - destinationDir := "/data" - volumeFlagStr := fmt.Sprintf("%s:%s", tmpDir, destinationDir) + testCase := nerdtest.Setup() - defer base.Cmd("rm", "-f", "-v", containerName).Run() - // 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", "--volume", volumeFlagStr, "--name", containerName, "alpine").AssertOK() - - container := base.InspectContainer(containerName) - errMsg := fmt.Sprintf("test failed, cannot find volume: %v", container.Mounts) - assert.Assert(t, container.Mounts != nil, errMsg) - assert.Assert(t, len(container.Mounts) == 1, errMsg) - assert.Assert(t, container.Mounts[0].Source == tmpDir, errMsg) - assert.Assert(t, container.Mounts[0].Destination == destinationDir, errMsg) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + composePath := data.Temp().Save(dockerComposeYAML, "compose.yaml") + projectName := filepath.Base(filepath.Dir(composePath)) + t.Logf("projectName=%q", projectName) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + volumeFlagStr := fmt.Sprintf("%s:%s", data.Temp().Path(), destinationDir) + cmd := helpers.Command( + "compose", + "-f", + data.Temp().Path("compose.yaml"), + "run", + "--volume", + volumeFlagStr, + "--name", + data.Identifier(), + "alpine", + ) + cmd.WithPseudoTTY() + return cmd + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, tt tig.T) { + container := nerdtest.InspectContainer(helpers, data.Identifier()) + errMsg := fmt.Sprintf("test failed, cannot find volume: %v", container.Mounts) + assert.Assert(tt, container.Mounts != nil, errMsg) + assert.Assert(tt, len(container.Mounts) == 1, errMsg) + assert.Assert(tt, container.Mounts[0].Source == data.Temp().Path(), errMsg) + assert.Assert(tt, container.Mounts[0].Destination == destinationDir, errMsg) + }, + } + } + + testCase.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 TestComposePushAndPullWithCosignVerify(t *testing.T) { diff --git a/cmd/nerdctl/container/container_create_linux_test.go b/cmd/nerdctl/container/container_create_linux_test.go index 1551cdf3555..81aa8be40c4 100644 --- a/cmd/nerdctl/container/container_create_linux_test.go +++ b/cmd/nerdctl/container/container_create_linux_test.go @@ -36,148 +36,280 @@ import ( "github.com/containerd/nerdctl/mod/tigron/test" "github.com/containerd/nerdctl/mod/tigron/tig" - "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" ) func TestCreateWithLabel(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - - base.Cmd("create", "--name", tID, "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo").AssertOK() - defer base.Cmd("rm", "-f", tID).Run() - inspect := base.InspectContainer(tID) - assert.Equal(base.T, "bar", inspect.Config.Labels["foo"]) - // the label `maintainer`` is defined by image - assert.Equal(base.T, "NGINX Docker Maintainers ", inspect.Config.Labels["maintainer"]) + testCase := nerdtest.Setup() + 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("create", "--name", data.Identifier(), "--label", "foo=bar", testutil.NginxAlpineImage, "echo", "foo") + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + fooLabel := strings.TrimSpace(helpers.Capture("inspect", "--format", `{{index .Config.Labels "foo"}}`, data.Identifier())) + assert.Equal(t, "bar", fooLabel) + maintainerLabel := strings.TrimSpace(helpers.Capture("inspect", "--format", `{{index .Config.Labels "maintainer"}}`, data.Identifier())) + assert.Equal(t, "NGINX Docker Maintainers ", maintainerLabel) + }, + } + } + testCase.Run(t) } func TestCreateWithMACAddress(t *testing.T) { - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - networkBridge := "testNetworkBridge" + tID - networkMACvlan := "testNetworkMACvlan" + tID - networkIPvlan := "testNetworkIPvlan" + tID - - tearDown := func() { - base.Cmd("network", "rm", networkBridge).Run() - base.Cmd("network", "rm", networkMACvlan).Run() - base.Cmd("network", "rm", networkIPvlan).Run() + testCase := nerdtest.Setup() + + const ( + networkBridgeKey = "networkBridge" + networkMACvlanKey = "networkMACvlan" + networkIPvlanKey = "networkIPvlan" + defaultMacKey = "defaultMac" + macAddressKey = "macAddress" + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set(networkBridgeKey, "testNetworkBridge"+data.Identifier()) + data.Labels().Set(networkMACvlanKey, "testNetworkMACvlan"+data.Identifier()) + data.Labels().Set(networkIPvlanKey, "testNetworkIPvlan"+data.Identifier()) + helpers.Ensure("network", "create", data.Labels().Get(networkBridgeKey), "--driver", "bridge") + helpers.Ensure("network", "create", data.Labels().Get(networkMACvlanKey), "--driver", "macvlan") + helpers.Ensure("network", "create", data.Labels().Get(networkIPvlanKey), "--driver", "ipvlan") + defaultMac := strings.TrimSpace(helpers.Capture("run", "--rm", "--network", "host", + testutil.CommonImage, "sh", "-c", "ip addr show eth0 | grep ether | awk '{printf $2}'")) + data.Labels().Set(defaultMacKey, defaultMac) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("network", "rm", data.Labels().Get(networkBridgeKey)) + helpers.Anyhow("network", "rm", data.Labels().Get(networkMACvlanKey)) + helpers.Anyhow("network", "rm", data.Labels().Get(networkIPvlanKey)) } - tearDown() - t.Cleanup(tearDown) - - base.Cmd("network", "create", networkBridge, "--driver", "bridge").AssertOK() - base.Cmd("network", "create", networkMACvlan, "--driver", "macvlan").AssertOK() - base.Cmd("network", "create", networkIPvlan, "--driver", "ipvlan").AssertOK() - - defaultMac := base.Cmd("run", "--rm", "-i", "--network", "host", testutil.CommonImage). - CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))). - Run().Stdout() - - passedMac := "we expect the generated mac on the output" - tests := []struct { - Network string - WantErr bool - Expect string - }{ - {"host", false, defaultMac}, // anything but the actual address being passed - {"none", false, ""}, - {"container:whatever" + tID, true, "container"}, // "No such container" vs. "could not find container" - {"bridge", false, passedMac}, - {networkBridge, false, passedMac}, - {networkMACvlan, false, passedMac}, - {networkIPvlan, true, "not support"}, + setupMAC := func(data test.Data, helpers test.Helpers) { + macAddress, err := nettestutil.GenerateMACAddress() + assert.NilError(helpers.T(), err, "failed to generate MAC address") + data.Labels().Set(macAddressKey, macAddress) } - for i, test := range tests { - containerName := fmt.Sprintf("%s_%d", tID, i) - testName := fmt.Sprintf("%s_container:%s_network:%s_expect:%s", tID, containerName, test.Network, test.Expect) - expect := test.Expect - network := test.Network - wantErr := test.WantErr - t.Run(testName, func(tt *testing.T) { - tt.Parallel() - - macAddress, err := nettestutil.GenerateMACAddress() - if err != nil { - tt.Errorf("failed to generate MAC address: %s", err) - } - if expect == passedMac { - expect = macAddress - } - tearDown := func() { - base.Cmd("rm", "-f", containerName).Run() - } - tearDown() - tt.Cleanup(tearDown) - // This is currently blocked by https://github.com/containerd/nerdctl/pull/3104 - // res := base.Cmd("create", "-i", "--network", network, "--mac-address", macAddress, testutil.CommonImage).Run() - res := base.Cmd("create", "--network", network, "--name", containerName, - "--mac-address", macAddress, testutil.CommonImage, - "sh", "-c", "--", "ip addr show").Run() - - if !wantErr { - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) - // This is currently blocked by: https://github.com/containerd/nerdctl/pull/3104 - // res = base.Cmd("start", "-i", containerName). - // CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() - res = base.Cmd("start", "-a", containerName).Run() - // FIXME: flaky - this has failed on the CI once, with the output NOT containing anything - // https://github.com/containerd/nerdctl/actions/runs/11392051487/job/31697214002?pr=3535#step:7:271 - assert.Assert(t, strings.Contains(res.Stdout(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Stdout())) - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded") - } else { - if nerdtest.IsDocker() && - (network == networkIPvlan || network == "container:whatever"+tID) { - // unlike nerdctl - // when using network ipvlan or container in Docker - // it delays fail on executing start command - assert.Assert(t, res.ExitCode == 0, "Command should have succeeded", res) - res = base.Cmd("start", "-i", "-a", containerName). - CmdOption(testutil.WithStdin(strings.NewReader("ip addr show eth0 | grep ether | awk '{printf $2}'"))).Run() - } - // See https://github.com/containerd/nerdctl/issues/3101 - if nerdtest.IsDocker() && - (network == networkBridge) { - expect = "" + makeCreateCommand := func(network string) test.Executor { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", + "--network", network, + "--name", data.Identifier(), + "--mac-address", data.Labels().Get(macAddressKey), + testutil.CommonImage, "sh", "-c", "--", "ip addr show") + } + } + makeDynamicCreateCommand := func(networkKey string) test.Executor { + return func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("create", + "--network", data.Labels().Get(networkKey), + "--name", data.Identifier(), + "--mac-address", data.Labels().Get(macAddressKey), + testutil.CommonImage, "sh", "-c", "--", "ip addr show") + } + } + + testCase.SubTests = []*test.Case{ + { + Description: "host network - container inherits host MAC", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("host"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(defaultMacKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(defaultMacKey), startOut)) + }, + } + }, + }, + { + Description: "none network - MAC address flag is accepted", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("none"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, !strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to not contain MAC %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "container network - nonexistent container fails", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("container:nonexistent-container-for-test"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if nerdtest.IsDocker() { + // Docker delays the failure to start time + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("container")}, + }) + }, + } + } + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("container")}, + } + }, + }, + { + Description: "bridge network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeCreateCommand("bridge"), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "custom bridge network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkBridgeKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, + } + }, + }, + { + Description: "macvlan network - MAC address is applied", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkMACvlanKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + startOut := helpers.Capture("start", "-a", data.Identifier()) + assert.Assert(t, strings.Contains(startOut, data.Labels().Get(macAddressKey)), + fmt.Sprintf("expected start output to contain %q: %q", data.Labels().Get(macAddressKey), startOut)) + }, } - if expect != "" { - assert.Assert(t, strings.Contains(res.Combined(), expect), fmt.Sprintf("expected output to contain %q: %q", expect, res.Combined())) - } else { - assert.Assert(t, res.Combined() == "", fmt.Sprintf("expected output to be empty: %q", res.Combined())) + }, + }, + { + Description: "ipvlan network - MAC address setting not supported", + Setup: setupMAC, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: makeDynamicCreateCommand(networkIPvlanKey), + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + if nerdtest.IsDocker() { + // Docker delays the failure to start time + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + helpers.Command("start", "-i", "-a", data.Identifier()). + Run(&test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("not support")}, + }) + }, + } + } + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("not support")}, } - assert.Assert(t, res.ExitCode != 0, "Command should have failed", res) - } - }) + }, + }, } + testCase.Run(t) } func TestCreateWithTty(t *testing.T) { - base := testutil.NewBase(t) - imageName := testutil.CommonImage - withoutTtyContainerName := "without-terminal-" + testutil.Identifier(t) - withTtyContainerName := "with-terminal-" + testutil.Identifier(t) - - // without -t, fail - base.Cmd("create", "--name", withoutTtyContainerName, imageName, "stty").AssertOK() - base.Cmd("start", withoutTtyContainerName).AssertOK() - defer base.Cmd("container", "rm", "-f", withoutTtyContainerName).AssertOK() - base.Cmd("logs", withoutTtyContainerName).AssertCombinedOutContains("stty: standard input: Not a tty") - withoutTtyContainer := base.InspectContainer(withoutTtyContainerName) - assert.Equal(base.T, 1, withoutTtyContainer.State.ExitCode) - - // with -t, success - base.Cmd("create", "-t", "--name", withTtyContainerName, imageName, "stty").AssertOK() - base.Cmd("start", withTtyContainerName).AssertOK() - defer base.Cmd("container", "rm", "-f", withTtyContainerName).AssertOK() - base.Cmd("logs", withTtyContainerName).AssertCombinedOutContains("speed 38400 baud; line = 0;") - withTtyContainer := base.InspectContainer(withTtyContainerName) - assert.Equal(base.T, 0, withTtyContainer.State.ExitCode) + testCase := nerdtest.Setup() + testCase.SubTests = []*test.Case{ + { + Description: "create without tty - stty exits with error", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--name", data.Identifier(), testutil.CommonImage, "stty") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", "-a", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("stty: standard input: Not a tty")}, + } + }, + }, + { + Description: "create with tty - stty succeeds", + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "-t", "--name", data.Identifier(), testutil.CommonImage, "stty") + // start without -a: tty output is not forwarded over a pipe, so + // capturing it via "start -a" is unreliable. Use "logs" instead, + // which reads from the containerd log driver regardless of tty. + helpers.Ensure("start", data.Identifier()) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("logs", data.Identifier()) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains("speed 38400 baud; line = 0;"), + } + }, + }, + } + testCase.Run(t) } // TestIssue2993 tests https://github.com/containerd/nerdctl/issues/2993 @@ -234,7 +366,7 @@ func TestIssue2993(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 1, + ExitCode: expect.ExitCodeGenericFail, Errors: []error{errors.New("is already used by ID")}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) @@ -281,7 +413,7 @@ func TestIssue2993(t *testing.T) { }, Expected: func(data test.Data, helpers test.Helpers) *test.Expected { return &test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, Errors: []error{}, Output: func(stdout string, t tig.T) { containersDirs, err := os.ReadDir(data.Labels().Get(containersPathKey)) @@ -301,110 +433,119 @@ func TestIssue2993(t *testing.T) { } func TestCreateFromOCIArchive(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - - // Docker does not support creating containers from OCI archive. - testutil.DockerIncompatible(t) - - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) - containerName := testutil.Identifier(t) - - teardown := func() { - base.Cmd("rm", "-f", containerName).Run() - base.Cmd("rmi", "-f", imageName).Run() - } - defer teardown() - teardown() + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.Build, + require.Not(nerdtest.Docker), + ) const sentinel = "test-nerdctl-create-from-oci-archive" - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tag := fmt.Sprintf("%s:latest", imageName) - tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) + data.Temp().Save(dockerfile, "Dockerfile") + + imageName := data.Identifier("image") + ":latest" + tarPath := data.Temp().Path("image.tar") + data.Labels().Set("imageName", imageName) + data.Labels().Set("tarPath", tarPath) - base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() - base.Cmd("create", "--rm", "--name", containerName, fmt.Sprintf("oci-archive://%s", tarPath)).AssertOK() - base.Cmd("start", "--attach", containerName).AssertOutContains("test-nerdctl-create-from-oci-archive") + helpers.Ensure("build", "--tag", imageName, + fmt.Sprintf("--output=type=oci,dest=%s", tarPath), + data.Temp().Path()) + helpers.Ensure("create", "--rm", "--name", data.Identifier(), + fmt.Sprintf("oci-archive://%s", tarPath)) + } + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + helpers.Anyhow("rmi", "-f", data.Labels().Get("imageName")) + } + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("start", "--attach", data.Identifier()) + } + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + assert.Assert(t, strings.Contains(stdout, sentinel), + fmt.Sprintf("expected stdout to contain %q: %q", sentinel, stdout)) + }, + } + } + testCase.Run(t) } 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") + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.AllowModifyUserns, + nerdtest.RemapIDs, + require.Not(nerdtest.Docker)) + testCase.NoParallel = true + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Labels().Set("validUserns", "nerdctltestuser") + data.Labels().Set("expectedHostUID", "123456789") + data.Labels().Set("invalidUserns", "invaliduser") + } + testCase.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(helpers.T(), err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(helpers.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: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.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")) + }, + } + }, }, - 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, t tig.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")) - }, - } - }, - }, - { - 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, - } - }, + { + 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(helpers.T(), err, "Failed to append Userns config") + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + removeUsernsConfig(helpers.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: expect.ExitCodeGenericFail, + } + }, + }, + { + 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: expect.ExitCodeGenericFail, + } }, }, } @@ -468,39 +609,37 @@ func appendUsernsConfig(userns string, hostUID string, helpers test.Helpers) err func addUser(username string, hostID string, helpers test.Helpers) { helpers.Custom("groupadd", "-g", hostID, username).Run(&test.Expected{ - ExitCode: 0}) + ExitCode: expect.ExitCodeSuccess}) helpers.Custom("useradd", "-u", hostID, "-g", hostID, "-s", "/bin/false", username).Run(&test.Expected{ - ExitCode: 0}) + ExitCode: expect.ExitCodeSuccess}) } -func removeUsernsConfig(t *testing.T, userns string, helpers test.Helpers) { +func removeUsernsConfig(t tig.T, userns string, helpers test.Helpers) { delUser(userns, helpers) delGroup(userns, helpers) - tempDir := helpers.T().TempDir() + tempDir := 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) + t.Log(fmt.Sprintf("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) + t.Log(fmt.Sprintf("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) + t.Log(fmt.Sprintf("failed to restore. Copy %s to %s failed, Error %s", fileBak, file, err)) continue } - } } diff --git a/cmd/nerdctl/container/container_health_check_linux_test.go b/cmd/nerdctl/container/container_health_check_linux_test.go index ebdf150fb26..15c012df5ee 100644 --- a/cmd/nerdctl/container/container_health_check_linux_test.go +++ b/cmd/nerdctl/container/container_health_check_linux_test.go @@ -20,6 +20,9 @@ import ( "encoding/json" "errors" "fmt" + "os" + "path/filepath" + "strconv" "strings" "testing" "time" @@ -1209,3 +1212,89 @@ func TestStartHealthcheckedContainerAfterExited(t *testing.T) { testCase.Run(t) } + +// TestHealthCheckDoesNotLeakShimPipeFDs ensures that running a health check does not leak anonymous pipe files in the containerd shim. +func TestHealthCheckDoesNotLeakShimPipeFDs(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(nerdtest.Rootless), + // Docker CLI does not provide a standalone healthcheck command. + require.Not(nerdtest.Docker), + ) + + 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", "-d", "--name", data.Identifier(), + "--health-cmd", "true", "--health-interval", "1h", + testutil.CommonImage, "sleep", nerdtest.Infinity, + ) + nerdtest.EnsureContainerStarted(helpers, data.Identifier()) + + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + shimPID, err := parentPID(inspect.State.Pid) + assert.NilError(helpers.T(), err) + + oldPipes, err := countShimPipeFDs(shimPID) + assert.NilError(helpers.T(), err) + + data.Labels().Set("shimPID", strconv.Itoa(shimPID)) + data.Labels().Set("oldPipes", strconv.Itoa(oldPipes)) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("container", "healthcheck", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + shimPID, _ := strconv.Atoi(data.Labels().Get("shimPID")) + oldPipes, _ := strconv.Atoi(data.Labels().Get("oldPipes")) + + newPipes, err := countShimPipeFDs(shimPID) + assert.NilError(t, err) + assert.Equal(t, oldPipes, newPipes, + "pipes leaked after health check: was %d, now %d", + oldPipes, newPipes) + }, + } + } + + testCase.Run(t) +} + +func parentPID(pid int) (int, error) { + data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)) + if err != nil { + return 0, err + } + for _, line := range strings.Split(string(data), "\n") { + if v, ok := strings.CutPrefix(line, "PPid:"); ok { + return strconv.Atoi(strings.TrimSpace(v)) + } + } + return 0, fmt.Errorf("PPid not found in /proc/%d/status", pid) +} + +func countShimPipeFDs(shimPID int) (int, error) { + fdDir := fmt.Sprintf("/proc/%d/fd", shimPID) + entries, err := os.ReadDir(fdDir) + if err != nil { + return 0, err + } + count := 0 + for _, e := range entries { + target, err := os.Readlink(filepath.Join(fdDir, e.Name())) + if err != nil { + continue + } + if strings.HasPrefix(target, "pipe:") { + count++ + } + } + return count, nil +} diff --git a/cmd/nerdctl/container/container_inspect_linux_test.go b/cmd/nerdctl/container/container_inspect_linux_test.go index 8f7d956a61c..6be9a2fd510 100644 --- a/cmd/nerdctl/container/container_inspect_linux_test.go +++ b/cmd/nerdctl/container/container_inspect_linux_test.go @@ -266,7 +266,7 @@ func TestContainerInspectContainsInternalLabel(t *testing.T) { lbs := inspect.Config.Labels // TODO: add more internal labels testcases - labelMount := lbs[labels.Mounts] + labelMount := labels.GetMount(lbs) expectedLabelMount := "[{\"Type\":\"bind\",\"Source\":\"/tmp\",\"Destination\":\"/app\",\"Mode\":\"rprivate,rbind\",\"RW\":true,\"Propagation\":\"rprivate\"}]" assert.Equal(tt, expectedLabelMount, labelMount) }) @@ -336,6 +336,7 @@ func TestContainerInspectState(t *testing.T) { } assert.Assert(tt, strings.Contains(inspect.State.Error, "executable file not found in $PATH"), fmt.Sprintf("expected: %s, actual: %s", "executable file not found in $PATH", inspect.State.Error)) assert.Equal(tt, expectedErrStatus, inspect.State.Status) + assert.Equal(tt, 0, inspect.State.Pid) }), }, { @@ -361,6 +362,7 @@ func TestContainerInspectState(t *testing.T) { inspect := dc[0] assert.Assert(tt, strings.Contains(inspect.State.Error, ""), fmt.Sprintf("expected: %s, actual: %s", "", inspect.State.Error)) assert.Equal(tt, "exited", inspect.State.Status) + assert.Equal(tt, 0, inspect.State.Pid) }), }, } diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 5daead7b8f4..b52994dd063 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -27,6 +27,7 @@ import ( "github.com/containerd/console" containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/runtime/restart" "github.com/containerd/log" "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" @@ -88,7 +89,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().Bool("help", false, "show help") cmd.Flags().BoolP("tty", "t", false, "Allocate a pseudo-TTY") - cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process (default true)") + cmd.Flags().Bool("sig-proxy", true, "Proxy received signals to the process") cmd.Flags().BoolP("interactive", "i", false, "Keep STDIN open even if not attached") cmd.Flags().String("restart", "no", `Restart policy to apply when a container exits (implemented values: "no"|"always|on-failure:n|unless-stopped")`) cmd.RegisterFlagCompletionFunc("restart", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -158,7 +159,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringP("memory", "m", "", "Memory limit") cmd.Flags().String("memory-reservation", "", "Memory soft limit") cmd.Flags().String("memory-swap", "", "Swap limit equal to memory plus swap: '-1' to enable unlimited swap") - cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100) (default -1)") + cmd.Flags().Int64("memory-swappiness", -1, "Tune container memory swappiness (0 to 100)") cmd.Flags().String("kernel-memory", "", "Kernel memory limit (deprecated)") cmd.Flags().Bool("oom-kill-disable", false, "Disable OOM Killer") cmd.Flags().Int("oom-score-adj", 0, "Tune container’s OOM preferences (-1000 to 1000, rootless: 100 to 1000)") @@ -218,7 +219,7 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().StringSlice("cap-drop", []string{}, "Drop Linux capabilities") cmd.RegisterFlagCompletionFunc("cap-drop", capShellComplete) cmd.Flags().Bool("privileged", false, "Give extended privileges to this container") - cmd.Flags().String("systemd", "false", "Allow running systemd in this container (default: false)") + cmd.Flags().String("systemd", "false", "Allow running systemd in this container") // #endregion // #region runtime flags @@ -249,9 +250,9 @@ func setCreateFlags(cmd *cobra.Command) { // Health check flags cmd.Flags().String("health-cmd", "", "Command to run to check health") - cmd.Flags().Duration("health-interval", 0, "Time between running the check (default: 30s)") - cmd.Flags().Duration("health-timeout", 0, "Maximum time to allow one check to run (default: 30s)") - cmd.Flags().Int("health-retries", 0, "Consecutive failures needed to report unhealthy (default: 3)") + cmd.Flags().Duration("health-interval", 0, "Time between running the check; 0 uses the image value or 30s when unset there too") + cmd.Flags().Duration("health-timeout", 0, "Maximum time to allow one check to run; 0 uses the image value or 30s when unset there too") + cmd.Flags().Int("health-retries", 0, "Consecutive failures needed to report unhealthy; 0 uses the image value or 3 when unset there too") cmd.Flags().Duration("health-start-period", 0, "Start period for the container to initialize before starting health-retries countdown") cmd.Flags().Bool("no-healthcheck", false, "Disable any container-specified HEALTHCHECK") @@ -467,6 +468,18 @@ func runAction(cmd *cobra.Command, args []string) error { return err } + // Set status label running should call after task is started. + _, restartPolicyExist := lab[restart.PolicyLabel] + if restartPolicyExist { + if err := containerutil.UpdateStatusLabel(ctx, c, containerd.Running); err != nil { + return err + } + } + + if err := containerutil.UpdateExplicitlyStoppedLabel(ctx, c, false); err != nil { + return err + } + // Setup container healthchecks. if err := healthcheck.CreateTimer(ctx, c, (*config.Config)(&createOpt.GOptions), createOpt.NerdctlCmd, createOpt.NerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) diff --git a/cmd/nerdctl/container/container_run_help_test.go b/cmd/nerdctl/container/container_run_help_test.go new file mode 100644 index 00000000000..9f8197200b5 --- /dev/null +++ b/cmd/nerdctl/container/container_run_help_test.go @@ -0,0 +1,47 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package container + +import ( + "bytes" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestRunHelpDoesNotDuplicateDefaults(t *testing.T) { + cmd := RunCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"--help"}) + + err := cmd.Execute() + assert.NilError(t, err) + + help := stdout.String() + assert.Assert(t, strings.Contains(help, "Proxy received signals to the process (default true)")) + assert.Assert(t, !strings.Contains(help, "Proxy received signals to the process (default true) (default true)")) + assert.Assert(t, strings.Contains(help, "Tune container memory swappiness (0 to 100) (default -1)")) + assert.Assert(t, !strings.Contains(help, "Tune container memory swappiness (0 to 100) (default -1) (default -1)")) + assert.Assert(t, strings.Contains(help, "Allow running systemd in this container (default \"false\")")) + assert.Assert(t, !strings.Contains(help, "Allow running systemd in this container (default: false) (default \"false\")")) + assert.Assert(t, strings.Contains(help, "Time between running the check; 0 uses the image value or 30s when unset there too")) + assert.Assert(t, strings.Contains(help, "Maximum time to allow one check to run; 0 uses the image value or 30s when unset there too")) + assert.Assert(t, strings.Contains(help, "Consecutive failures needed to report unhealthy; 0 uses the image value or 3 when unset there too")) +} diff --git a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go index 2f263c61992..2521ea6f709 100644 --- a/cmd/nerdctl/container/container_run_log_driver_syslog_test.go +++ b/cmd/nerdctl/container/container_run_log_driver_syslog_test.go @@ -18,8 +18,8 @@ package container import ( "fmt" + "io" "os" - "runtime" "strconv" "strings" "testing" @@ -27,102 +27,201 @@ import ( syslog "github.com/yuchanns/srslog" + "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/rootlessutil" "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/testsyslog" ) -func runSyslogTest(t *testing.T, networks []string, syslogFacilities map[string]syslog.Priority, fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error) { - if runtime.GOOS == "windows" { - t.Skip("syslog container logging is not officially supported on Windows") - } +// syslogCombination holds one entry of the (network x facility x format) cross product. +type syslogCombination struct { + network string + fPriK string + fPriV syslog.Priority + fmtK string + fmtValidFunc func(string, string, string, string, syslog.Priority, bool) error +} + +// buildSyslogSubTests expands the (network x facility x format) cross product +// into independent Tigron sub-cases. Each sub-case starts its own syslog +// listener in Command (immediately before the container launch) to avoid the +// 300ms goroutine timeout in runPacketSyslog expiring before the container +// sends its first log entry. Validation happens in Cleanup. +func buildSyslogSubTests( + networks []string, + syslogFacilities map[string]syslog.Priority, + fmtValidFuncs map[string]func(string, string, string, string, syslog.Priority, bool) error, + caRef **testca.CA, + certRef **testca.Cert, + hostnameRef *string, +) []*test.Case { + var combinations []syslogCombination - base := testutil.NewBase(t) - base.Cmd("pull", "--quiet", testutil.CommonImage).AssertOK() - hostname, err := os.Hostname() - if err != nil { - t.Fatalf("Error retrieving hostname") - } - ca := testca.New(base.T) - cert := ca.NewCert("127.0.0.1") - t.Cleanup(func() { - cert.Close() - ca.Close() - }) - rI := 0 for _, network := range networks { for rFK, rFV := range syslogFacilities { - fPriV := rFV - // test both string and number facility - for _, fPriK := range []string{rFK, strconv.Itoa(int(fPriV) >> 3)} { + for _, fPriK := range []string{rFK, strconv.Itoa(int(rFV) >> 3)} { for fmtK, fmtValidFunc := range fmtValidFuncs { - fmtKT := "empty" - if fmtK != "" { - fmtKT = fmtK - } - subTestName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(network, "+", "_"), fPriK, fmtKT) - i := rI - rI++ - t.Run(subTestName, func(t *testing.T) { - tID := testutil.Identifier(t) - tag := tID + "_syslog_driver" - msg := "hello, " + tID + "_syslog_driver" - if !testsyslog.TestableNetwork(network) { - if rootlessutil.IsRootless() { - t.Skipf("skipping on %s/%s; '%s' for rootless containers are not supported", runtime.GOOS, runtime.GOARCH, network) - } - t.Skipf("skipping on %s/%s; '%s' is not supported", runtime.GOOS, runtime.GOARCH, network) - } - testContainerName := fmt.Sprintf("%s-%d-%s", tID, i, fPriK) - done := make(chan string) - addr, closer := testsyslog.StartServer(network, "", done, cert) - args := []string{ - "run", - "-d", - "--name", - testContainerName, - "--restart=no", - "--log-driver=syslog", - "--log-opt=syslog-facility=" + fPriK, - "--log-opt=tag=" + tag, - "--log-opt=syslog-format=" + fmtK, - "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", network, addr), - } - if network == "tcp+tls" { - args = append(args, - "--log-opt=syslog-tls-cert="+cert.CertPath, - "--log-opt=syslog-tls-key="+cert.KeyPath, - "--log-opt=syslog-tls-ca-cert="+ca.CertPath, - ) - } - args = append(args, testutil.CommonImage, "echo", msg) - base.Cmd(args...).AssertOK() - t.Cleanup(func() { - base.Cmd("rm", "-f", testContainerName).AssertOK() - }) - defer closer.Close() - defer close(done) - select { - case rcvd := <-done: - if err := fmtValidFunc(rcvd, msg, tag, hostname, fPriV, network == "tcp+tls"); err != nil { - t.Error(err) - } - case <-time.Tick(time.Second * 3): - t.Errorf("timeout with %s", subTestName) - } + combinations = append(combinations, syslogCombination{ + network: network, + fPriK: fPriK, + fPriV: rFV, + fmtK: fmtK, + fmtValidFunc: fmtValidFunc, }) } } } } + + var cases []*test.Case + + for _, c := range combinations { + fmtKT := "empty" + if c.fmtK != "" { + fmtKT = c.fmtK + } + subName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(c.network, "+", "_"), c.fPriK, fmtKT) + + var ( + addr string + done chan string + closer io.Closer + containerName string + tag string + msg string + ) + + cases = append(cases, &test.Case{ + Description: subName, + // runPacketSyslog reads with 4x100ms deadlines (~400ms total). + // Parallel execution on slow arm runners pushes container startup + // past that window, causing the server to send "" on the channel. + // Sequential matches the original t.Run-based test behaviour. + NoParallel: true, + Setup: func(data test.Data, helpers test.Helpers) { + if !testsyslog.TestableNetwork(c.network) { + if rootlessutil.IsRootless() { + helpers.T().Skip(fmt.Sprintf("%q for rootless containers is not supported", c.network)) + } + helpers.T().Skip(fmt.Sprintf("%q is not supported", c.network)) + } + tID := data.Identifier() + tag = tID + "_syslog_driver" + msg = "hello, " + tID + "_syslog_driver" + containerName = fmt.Sprintf("%s-%s", tID, c.fPriK) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + // Start the server here, immediately before launching the + // container, so the 300ms goroutine timeout in + // runPacketSyslog does not expire before the container + // produces its first log entry. + done = make(chan string) + addr, closer = testsyslog.StartServer(c.network, "", done, *certRef) + args := []string{ + "run", + "-d", + "--name", containerName, + "--restart=no", + "--log-driver=syslog", + "--log-opt=syslog-facility=" + c.fPriK, + "--log-opt=tag=" + tag, + "--log-opt=syslog-format=" + c.fmtK, + "--log-opt=syslog-address=" + fmt.Sprintf("%s://%s", c.network, addr), + } + if c.network == "tcp+tls" { + cert := *certRef + ca := *caRef + args = append(args, + "--log-opt=syslog-tls-cert="+cert.CertPath, + "--log-opt=syslog-tls-key="+cert.KeyPath, + "--log-opt=syslog-tls-ca-cert="+ca.CertPath, + ) + } + args = append(args, testutil.CommonImage, "echo", msg) + return helpers.Command(args...) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, nil), + Cleanup: func(data test.Data, helpers test.Helpers) { + if containerName != "" { + helpers.Anyhow("rm", "-f", containerName) + } + if closer == nil || done == nil { + return + } + defer closer.Close() + defer close(done) + select { + case rcvd := <-done: + if err := c.fmtValidFunc(rcvd, msg, tag, *hostnameRef, c.fPriV, c.network == "tcp+tls"); err != nil { + helpers.T().Log(err) + helpers.T().Fail() + } + case <-time.After(time.Second * 3): + helpers.T().Log(fmt.Sprintf("timeout with %s", subName)) + helpers.T().Fail() + } + }, + }) + } + + return cases +} + +// newSyslogTestCase wires the shared outer fixture: skip on Windows, pull the +// image, generate a CA/cert pair, and expose them to the sub-cases via the +// returned pointers. +func newSyslogTestCase(t *testing.T) (*test.Case, **testca.CA, **testca.Cert, *string) { + t.Helper() + + testCase := &test.Case{ + Require: require.Not(require.OS("windows")), + } + + var ( + ca *testca.CA + cert *testca.Cert + hostname string + ) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + hn, err := os.Hostname() + if err != nil { + helpers.T().Log(fmt.Sprintf("retrieving hostname: %v", err)) + helpers.T().FailNow() + } + hostname = hn + ca = testca.New(t) + cert = ca.NewCert("127.0.0.1") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if cert != nil { + cert.Close() + } + if ca != nil { + ca.Close() + } + } + + return testCase, &ca, &cert, &hostname } func TestSyslogNetwork(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "user": syslog.LOG_USER, } - networks := []string{ "udp", "tcp", @@ -131,28 +230,22 @@ func TestSyslogNetwork(t *testing.T) { "unixgram", } fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "rfc5424": rfc5424Validator, } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) } func TestSyslogFacilities(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "kern": syslog.LOG_KERN, "user": syslog.LOG_USER, "mail": syslog.LOG_MAIL, @@ -174,86 +267,72 @@ func TestSyslogFacilities(t *testing.T) { "local6": syslog.LOG_LOCAL6, "local7": syslog.LOG_LOCAL7, } - networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "rfc5424": rfc5424Validator, } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) } func TestSyslogFormat(t *testing.T) { - var syslogFacilities = map[string]syslog.Priority{ + base := nerdtest.Setup() + tc, caRef, certRef, hostnameRef := newSyslogTestCase(t) + base.Require = tc.Require + base.Setup = tc.Setup + base.Cleanup = tc.Cleanup + + syslogFacilities := map[string]syslog.Priority{ "user": syslog.LOG_USER, } - networks := []string{"unix"} fmtValidFuncs := map[string]func(string, string, string, string, syslog.Priority, bool) error{ - "": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isSTLS bool) error { - var mon, day, hrs string - var pid int - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - return nil - }, - "rfc3164": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, mon, day, hrs string - var pid int - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - return nil - }, - "rfc5424": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, - "rfc5424micro": func(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { - var parsedHostname, timestamp string - var length, version, pid int - if !isTLS { - exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } else { - exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" - if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { - return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) - } - } - return nil - }, + "": emptyFormatValidator, + "rfc3164": rfc3164Validator, + "rfc5424": rfc5424Validator, + "rfc5424micro": rfc5424Validator, + } + + base.SubTests = buildSyslogSubTests(networks, syslogFacilities, fmtValidFuncs, caRef, certRef, hostnameRef) + + base.Run(t) +} + +func rfc5424Validator(rcvd, msg, tag, hostname string, pri syslog.Priority, isTLS bool) error { + var parsedHostname, timestamp string + var length, version, pid int + if !isTLS { + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &version, ×tamp, &parsedHostname, &pid); n != 4 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil + } + exp := "%d " + fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%d %s %s " + tag + " %d " + tag + " - " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &length, &version, ×tamp, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil +} + +func rfc3164Validator(rcvd, msg, tag, hostname string, pri syslog.Priority, _ bool) error { + var parsedHostname, mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &parsedHostname, &pid); n != 5 || err != nil || hostname != parsedHostname { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) + } + return nil +} + +func emptyFormatValidator(rcvd, msg, tag, _ string, pri syslog.Priority, _ bool) error { + var mon, day, hrs string + var pid int + exp := fmt.Sprintf("<%d>", pri|syslog.LOG_INFO) + "%s %s %s " + tag + "[%d]: " + msg + "\n" + if n, err := fmt.Sscanf(rcvd, exp, &mon, &day, &hrs, &pid); n != 4 || err != nil { + return fmt.Errorf("s.Info() = '%q', didn't match '%q' (%d %s)", rcvd, exp, n, err) } - runSyslogTest(t, networks, syslogFacilities, fmtValidFuncs) + return nil } diff --git a/cmd/nerdctl/container/container_run_mount_linux_test.go b/cmd/nerdctl/container/container_run_mount_linux_test.go index f66f62e46b2..083ddb47adf 100644 --- a/cmd/nerdctl/container/container_run_mount_linux_test.go +++ b/cmd/nerdctl/container/container_run_mount_linux_test.go @@ -32,6 +32,7 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/mountutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/testutil" "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" @@ -762,3 +763,30 @@ func TestBindMountWhenHostFolderDoesNotExist(t *testing.T) { _, err = os.Stat(hp) assert.ErrorIs(t, err, os.ErrNotExist) } + +func TestRunVolumeWithRootDestination(t *testing.T) { + testCase := nerdtest.Setup() + + 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("run", "-d", "--name", data.Identifier(), + "-v", data.Temp().Dir()+":/", testutil.AlpineImage) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{mountutil.ErrVolumeTargetIsRoot}, + Output: func(stdout string, t tig.T) { + psOutput := helpers.Capture("ps", "-a", "--format", "{{.Names}}") + assert.Assert(t, !strings.Contains(psOutput, data.Identifier()), + "no container should be created when the volume destination is '/'") + }, + } + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run_mount_windows_test.go b/cmd/nerdctl/container/container_run_mount_windows_test.go index b0e6afde18a..d75ef3f221e 100644 --- a/cmd/nerdctl/container/container_run_mount_windows_test.go +++ b/cmd/nerdctl/container/container_run_mount_windows_test.go @@ -17,14 +17,21 @@ package container import ( + "errors" "fmt" "os" + "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/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" ) func TestRunMountVolume(t *testing.T) { @@ -213,3 +220,30 @@ func TestRunMountVolumeSpec(t *testing.T) { // If -v is an empty string, it will be ignored base.Cmd("run", "--rm", "-v", "", testutil.CommonImage).AssertOK() } + +func TestRunVolumeWithDriveRootDestination(t *testing.T) { + testCase := nerdtest.Setup() + + 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("run", "-d", "--name", data.Identifier(), + "-v", data.Temp().Dir()+`:C:\.`, testutil.CommonImage) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New("destination path (c:\\\\) cannot be 'c:' or 'c:\\\\'")}, + Output: func(stdout string, t tig.T) { + psOutput := helpers.Capture("ps", "-a", "--format", "{{.Names}}") + assert.Assert(t, !strings.Contains(psOutput, data.Identifier()), + "no container should be created when the volume destination is the drive root") + }, + } + } + + testCase.Run(t) +} diff --git a/cmd/nerdctl/container/container_run_restart_linux_test.go b/cmd/nerdctl/container/container_run_restart_linux_test.go index 795550696f6..17761188e39 100644 --- a/cmd/nerdctl/container/container_run_restart_linux_test.go +++ b/cmd/nerdctl/container/container_run_restart_linux_test.go @@ -19,42 +19,119 @@ package container import ( "fmt" "io" + "os" "os/exec" + "strconv" "strings" "testing" "time" "gotest.tools/v3/assert" - "gotest.tools/v3/poll" + "github.com/containerd/containerd/v2/core/runtime/restart" + "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/dockercompat" "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/portlock" ) -func TestRunRestart(t *testing.T) { +func daemonSystemctlTarget() string { + if nerdtest.IsDocker() { + return "docker.service" + } + return "containerd.service" +} + +func daemonSystemctlArgs() []string { + if os.Geteuid() != 0 { + return []string{"--user"} + } + return nil +} + +func killDaemon(t tig.T) { + t.Helper() + target := daemonSystemctlTarget() + t.Log(fmt.Sprintf("killing %q", target)) + cmd := exec.Command("systemctl", append(daemonSystemctlArgs(), "kill", target)...) + if out, err := cmd.CombinedOutput(); err != nil { + t.Log(fmt.Sprintf("cannot kill %q: %q: %v", target, string(out), err)) + t.FailNow() + } + // the daemon should restart automatically +} + +func ensureDaemonActive(t tig.T) { + t.Helper() + target := daemonSystemctlTarget() + t.Log(fmt.Sprintf("checking activity of %q", target)) const ( - hostPort = 8080 + maxRetry = 30 + sleep = 3 * time.Second ) - testContainerName := testutil.Identifier(t) + for i := 0; i < maxRetry; i++ { + cmd := exec.Command("systemctl", append(daemonSystemctlArgs(), "is-active", target)...) + out, err := cmd.CombinedOutput() + t.Log(fmt.Sprintf("(retry=%d) %s", i, string(out))) + if err == nil { + // The daemon is now running, but the daemon may still refuse connections to containerd.sock + t.Log(fmt.Sprintf("daemon %q is now running, checking whether the daemon can handle requests", target)) + infoOut, infoErr := exec.Command(testutil.GetTarget(), "info").CombinedOutput() + if infoErr == nil { + t.Log(fmt.Sprintf("daemon %q can now handle requests", target)) + return + } + t.Log(fmt.Sprintf("(retry=%d) info failed: %s: %v", i, string(infoOut), infoErr)) + } + time.Sleep(sleep) + } + t.Log(fmt.Sprintf("daemon %q not running?", target)) + t.FailNow() +} + +func dumpDaemonLogs(t tig.T, minutes int) { + t.Helper() + target := daemonSystemctlTarget() + cmd := exec.Command("journalctl", + append(daemonSystemctlArgs(), "-u", target, "--no-pager", "-S", fmt.Sprintf("%d min ago", minutes))...) + t.Log(fmt.Sprintf("===== %v =====", cmd.Args)) + out, err := cmd.CombinedOutput() + if err != nil { + t.Log(fmt.Sprintf("failed to dump daemon logs: %v", err)) + return + } + t.Log(string(out)) + t.Log("==========") +} + +// assertRestartCount asserts that `container inspect` reports the expected RestartCount. +func assertRestartCount(expected int) test.Comparator { + return expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Equal(t, dc[0].RestartCount, expected) + }) +} + +func TestRunRestart(t *testing.T) { if testing.Short() { t.Skipf("test is long") } - base := testutil.NewBase(t) - if !base.DaemonIsKillable { + if !testutil.GetDaemonIsKillable() { t.Skip("daemon is not killable (hint: set \"-test.allow-kill-daemon\")") } t.Log("NOTE: this test may take a while") - defer base.Cmd("rm", "-f", testContainerName).Run() - - base.Cmd("run", "-d", - "--restart=always", - "--name", testContainerName, - "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), - testutil.NginxAlpineImage).AssertOK() + testCase := nerdtest.Setup() + testCase.NoParallel = true - check := func(httpGetRetry int) error { + httpCheck := func(data test.Data, httpGetRetry int) error { + hostPort, _ := strconv.Atoi(data.Labels().Get("hostPort")) resp, err := nettestutil.HTTPGet(fmt.Sprintf("http://127.0.0.1:%d", hostPort), httpGetRetry, false) if err != nil { return err @@ -69,117 +146,239 @@ func TestRunRestart(t *testing.T) { } return nil } - assert.NilError(t, check(5)) - base.KillDaemon() - base.EnsureDaemonActive() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + port, err := portlock.Acquire(0) + if err != nil { + helpers.T().Log(fmt.Sprintf("failed to acquire port: %v", err)) + helpers.T().FailNow() + } + data.Labels().Set("hostPort", strconv.Itoa(port)) - const ( - maxRetry = 30 - sleep = 3 * time.Second - ) - for i := 0; i < maxRetry; i++ { - t.Logf("(retry %d) ps -a: %q", i, base.Cmd("ps", "-a").Run().Combined()) - err := check(1) + helpers.Ensure("run", "-d", + "--restart=always", + "--name", data.Identifier(), + "-p", fmt.Sprintf("127.0.0.1:%d:80", port), + testutil.NginxAlpineImage) + + assert.NilError(helpers.T(), httpCheck(data, 5)) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + port, err := strconv.Atoi(data.Labels().Get("hostPort")) if err == nil { - t.Logf("test is passing, after %d retries", i) - return + portlock.Release(port) } - time.Sleep(sleep) } - base.DumpDaemonLogs(10) - t.Fatalf("the container does not seem to be restarted") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + killDaemon(helpers.T()) + ensureDaemonActive(helpers.T()) + return helpers.Command("ps", "-a") + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + t.Log(fmt.Sprintf("initial ps -a: %q", stdout)) + + const ( + maxRetry = 30 + sleep = 3 * time.Second + ) + for i := 0; i < maxRetry; i++ { + err := httpCheck(data, 1) + if err == nil { + t.Log(fmt.Sprintf("test is passing, after %d retries", i)) + return + } + time.Sleep(sleep) + t.Log(fmt.Sprintf("(retry %d) ps -a: %q", i, helpers.Capture("ps", "-a"))) + } + dumpDaemonLogs(t, 10) + t.Log("the container does not seem to be restarted") + t.FailNow() + }, + } + } + + testCase.Run(t) } func TestRunRestartWithOnFailure(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=on-failure:2", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=on-failure:2", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + 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 { + nerdtest.EnsureContainerExited(helpers, data.Identifier(), -1) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } func TestRunRestartWithUnlessStopped(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"unless-stopped"}) + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"unless-stopped"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=unless-stopped", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=unless-stopped", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + if inspect.State != nil && inspect.State.Status == "exited" { + break + } + if inspect.RestartCount == 2 { + helpers.Ensure("stop", data.Identifier()) + } + time.Sleep(100 * time.Millisecond) } - if inspect.RestartCount == 2 { - base.Cmd("stop", tID).AssertOK() + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } func TestUpdateRestartPolicy(t *testing.T) { - base := testutil.NewBase(t) - if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) - } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--restart=on-failure:1", "--name", tID, testutil.AlpineImage, "sh", "-c", "exit 1").AssertOK() - base.Cmd("update", "--restart=on-failure:2", tID).AssertOK() - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "exited" { - return poll.Success() + testCase := nerdtest.Setup() + if nerdtest.IsDocker() { + // FIXME: failing on Docker since ubuntu-24.04 image 20260615.205.1 + // https://github.com/containerd/nerdctl/issues/4978 + testCase.Require = require.Not(nerdtest.Docker) + } else { + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--restart=on-failure:1", "--name", data.Identifier(), testutil.AlpineImage, "sh", "-c", "exit 1") + helpers.Ensure("update", "--restart=on-failure:2", data.Identifier()) + } + + 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 { + nerdtest.EnsureContainerExited(helpers, data.Identifier(), -1) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(2), } - return poll.Continue("container is not yet exited") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect := base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 2) + + testCase.Run(t) } // The test is to add a restart policy to a container which has not restart policy before, // and check it can work correctly. func TestAddRestartPolicy(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + if !nerdtest.IsDocker() { + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"on-failure"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--name", data.Identifier(), testutil.NginxAlpineImage) + helpers.Ensure("update", "--restart=on-failure", data.Identifier()) + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("originalPid", strconv.Itoa(inspect.State.Pid)) + exec.Command("kill", "-9", strconv.Itoa(inspect.State.Pid)).Run() + } + + 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 { + originalPid, _ := strconv.Atoi(data.Labels().Get("originalPid")) + deadline := time.Now().Add(60 * time.Second) + for time.Now().Before(deadline) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + if inspect.State != nil && inspect.State.Status == "running" && inspect.State.Pid != originalPid { + break + } + time.Sleep(100 * time.Millisecond) + } + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: assertRestartCount(1), + } + } + + testCase.Run(t) +} + +func TestRunRestartStatusLabel(t *testing.T) { + testCase := nerdtest.Setup() if !nerdtest.IsDocker() { - testutil.RequireContainerdPlugin(base, "io.containerd.internal.v1", "restart", []string{"on-failure"}) - } - tID := testutil.Identifier(t) - defer base.Cmd("rm", "-f", tID).Run() - base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK() - base.Cmd("update", "--restart=on-failure", tID).AssertOK() - inspect := base.InspectContainer(tID) - orgialPid := inspect.State.Pid - exec.Command("kill", "-9", fmt.Sprintf("%v", orgialPid)).Run() - - check := func(log poll.LogT) poll.Result { - inspect := base.InspectContainer(tID) - if inspect.State != nil && inspect.State.Status == "running" && inspect.State.Pid != orgialPid { - return poll.Success() + testCase.Require = nerdtest.ContainerdPlugin("io.containerd.internal.v1", "restart", []string{"always"}) + } + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("create", "--restart=always", "--name", data.Identifier(), testutil.CommonImage, "sleep", "infinity") + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.JSON([]dockercompat.Container{}, func(dc []dockercompat.Container, t tig.T) { + assert.Equal(t, 1, len(dc)) + assert.Assert(t, dc[0].Config.Labels[restart.StatusLabel] == "") + }), } - return poll.Continue("container is not yet running") } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(60*time.Second)) - inspect = base.InspectContainer(tID) - assert.Equal(t, inspect.RestartCount, 1) + + testCase.Run(t) } diff --git a/cmd/nerdctl/container/container_run_security_linux_test.go b/cmd/nerdctl/container/container_run_security_linux_test.go index dd99f2d4699..3f0a6e0c891 100644 --- a/cmd/nerdctl/container/container_run_security_linux_test.go +++ b/cmd/nerdctl/container/container_run_security_linux_test.go @@ -345,6 +345,10 @@ func TestRunSelinuxWithVolumeLabel(t *testing.T) { func TestRunSeccompCapSysPtrace(t *testing.T) { testCase := nerdtest.Setup() + // FIXME: failing on Docker since ubuntu-24.04 image 20260615.205.1 + // https://github.com/containerd/nerdctl/issues/4978 + testCase.Require = require.Not(nerdtest.Docker) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { return helpers.Command("run", "--rm", "--cap-add", "sys_ptrace", testutil.AlpineImage, "sh", "-euxc", "apk add -q strace && strace true") } diff --git a/cmd/nerdctl/container/container_run_test.go b/cmd/nerdctl/container/container_run_test.go index ea424a2c889..8ad9c6d8c21 100644 --- a/cmd/nerdctl/container/container_run_test.go +++ b/cmd/nerdctl/container/container_run_test.go @@ -17,8 +17,6 @@ package container import ( - "bufio" - "bytes" "errors" "fmt" "os" @@ -32,15 +30,12 @@ import ( "gotest.tools/v3/assert" "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/mod/tigron/tig" - "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" ) @@ -309,175 +304,237 @@ func TestRunStdin(t *testing.T) { } func TestRunWithJsonFileLogDriver(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("json-file log driver is not yet implemented on Windows") + testCase := nerdtest.Setup() + 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", "max-size=5K", "--log-opt", "max-file=2", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000") } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() - - time.Sleep(3 * time.Second) - inspectedContainer := base.InspectContainer(containerName) - logJSONPath := filepath.Dir(inspectedContainer.LogPath) - // matches = current log file + old log files to retain - matches, err := filepath.Glob(filepath.Join(logJSONPath, inspectedContainer.ID+"*")) - assert.NilError(t, err) - if len(matches) != 2 { - t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - for _, file := range matches { - fInfo, err := os.Stat(file) - assert.NilError(t, err) - // The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility. - // Docker log rotation lacks precision because the size check is done at the log entry level - // and not at the byte level (io.Writer), so docker log files can exceed 5k - if fInfo.Size() > 5200 { - t.Fatal("file size exceeded 5k") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) + } + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + logJSONPath := filepath.Dir(inspect.LogPath) + // matches = current log file + old log files to retain + matches, err := filepath.Glob(filepath.Join(logJSONPath, inspect.ID+"*")) + assert.NilError(t, err) + assert.Equal(t, len(matches), 2, "the number of log files is not equal to 2 files, got: %v", matches) + for _, file := range matches { + fInfo, err := os.Stat(file) + assert.NilError(t, err) + // The log file size is compared to 5200 bytes (instead 5k) to keep docker compatibility. + // Docker log rotation lacks precision because the size check is done at the log entry level + // and not at the byte level (io.Writer), so docker log files can exceed 5k + assert.Assert(t, fInfo.Size() <= 5200, "file size exceeded 5k: %s", file) + } + }, } } + + testCase.Run(t) } func TestRunWithJsonFileLogDriverAndLogPathOpt(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("json-file log driver is not yet implemented on Windows") + testCase := nerdtest.Setup() + testCase.Require = require.All(require.Not(require.Windows), require.Not(nerdtest.Docker)) + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + customLogJSONPath := filepath.Join(data.Temp().Path(), data.Identifier(), data.Identifier()+"-json.log") + data.Labels().Set("logPath", customLogJSONPath) + helpers.Ensure("run", "-d", "--log-driver", "json-file", + "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), + "--log-opt", "max-size=5K", "--log-opt", "max-file=2", + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000") } - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - customLogJSONPath := filepath.Join(t.TempDir(), containerName, containerName+"-json.log") - base.Cmd("run", "-d", "--log-driver", "json-file", "--log-opt", fmt.Sprintf("log-path=%s", customLogJSONPath), "--log-opt", "max-size=5K", "--log-opt", "max-file=2", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "hexdump -C /dev/urandom | head -n1000").AssertOK() - - time.Sleep(3 * time.Second) - rawBytes, err := os.ReadFile(customLogJSONPath) - assert.NilError(t, err) - if len(rawBytes) == 0 { - t.Fatalf("logs are not written correctly to log-path: %s", customLogJSONPath) + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - // matches = current log file + old log files to retain - matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), containerName+"*")) - assert.NilError(t, err) - if len(matches) != 2 { - t.Fatalf("the number of log files is not equal to 2 files, got: %s", matches) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + time.Sleep(3 * time.Second) + return helpers.Command("inspect", data.Identifier()) } - for _, file := range matches { - fInfo, err := os.Stat(file) - assert.NilError(t, err) - if fInfo.Size() > 5200 { - t.Fatal("file size exceeded 5k") + + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + customLogJSONPath := data.Labels().Get("logPath") + rawBytes, err := os.ReadFile(customLogJSONPath) + assert.NilError(t, err) + assert.Assert(t, len(rawBytes) > 0, "logs are not written correctly to log-path: %s", customLogJSONPath) + // matches = current log file + old log files to retain + matches, err := filepath.Glob(filepath.Join(filepath.Dir(customLogJSONPath), data.Identifier()+"*")) + assert.NilError(t, err) + assert.Equal(t, len(matches), 2, "the number of log files is not equal to 2 files, got: %v", matches) + for _, file := range matches { + fInfo, err := os.Stat(file) + assert.NilError(t, err) + assert.Assert(t, fInfo.Size() <= 5200, "file size exceeded 5k: %s", file) + } + }, } } + + testCase.Run(t) } -func TestRunWithJournaldLogDriver(t *testing.T) { - testutil.RequireExecutable(t, "journalctl") +func waitForJournaldLogs(since, filter string, expected ...string) { 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()) + deadline := time.Now().Add(20 * time.Second) + for time.Now().Before(deadline) { + res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", since, filter)) + found := true + for _, s := range expected { + if !strings.Contains(res.Stdout(), s) { + found = false + break + } + } + if found { + break + } + time.Sleep(100 * time.Millisecond) } +} - if runtime.GOOS == "windows" { - t.Skip("journald log driver is not yet implemented on Windows") +func journaldRequire() *test.Requirement { + return require.All( + require.Not(require.Windows), + require.Binary("journalctl"), + &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + journalctl, _ := exec.LookPath("journalctl") + res := icmd.RunCmd(icmd.Command(journalctl, "-xe")) + if res.ExitCode != expect.ExitCodeSuccess { + return false, fmt.Sprintf("current user is not allowed to access journal logs: %s", res.Combined()) + } + return true, "journald is accessible" + }, + }, + ) +} + +func journaldExpected() func(data test.Data, helpers test.Helpers) *test.Expected { + return func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("foo"), + expect.Contains("bar"), + ), + } } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) +} - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "journald", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() +func TestRunWithJournaldLogDriver(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = journaldRequire() - time.Sleep(3 * time.Second) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + startTime := time.Now().Format("2006-01-02 15:04:05") + helpers.Ensure("run", "-d", "--log-driver", "journald", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("startTime", startTime) + data.Labels().Set("shortID", inspect.ID[:12]) + data.Labels().Set("containerName", data.Identifier()) + } - inspectedContainer := base.InspectContainer(containerName) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + } - type testCase struct { - name string - filter string + type journaldTC struct { + description string + filter func(data test.Data) string } - testCases := []testCase{ + + tcs := []journaldTC{ { - name: "filter journald logs using SYSLOG_IDENTIFIER field", - filter: fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID[:12]), + description: "filter journald logs using SYSLOG_IDENTIFIER field", + filter: func(data test.Data) string { return fmt.Sprintf("SYSLOG_IDENTIFIER=%s", data.Labels().Get("shortID")) }, }, { - name: "filter journald logs using CONTAINER_NAME field", - filter: fmt.Sprintf("CONTAINER_NAME=%s", containerName), + description: "filter journald logs using CONTAINER_NAME field", + filter: func(data test.Data) string { + return fmt.Sprintf("CONTAINER_NAME=%s", data.Labels().Get("containerName")) + }, }, { - name: "filter journald logs using IMAGE_NAME field", - filter: fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage), + description: "filter journald logs using IMAGE_NAME field", + filter: func(data test.Data) string { return fmt.Sprintf("IMAGE_NAME=%s", testutil.CommonImage) }, }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - found := 0 - check := func(log poll.LogT) poll.Result { - res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", tc.filter)) - assert.Equal(t, 0, res.ExitCode, res) - if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { - found = 1 - return poll.Success() - } - return poll.Continue("reading from journald is not yet finished") - } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) - assert.Equal(t, 1, found) + + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + journalctl, _ := exec.LookPath("journalctl") + filter := tc.filter(data) + since := data.Labels().Get("startTime") + waitForJournaldLogs(since, filter, "foo", "bar") + return helpers.Custom(journalctl, "--no-pager", "--since", since, filter) + }, + Expected: journaldExpected(), }) } + + testCase.Run(t) } func TestRunWithJournaldLogDriverAndLogOpt(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()) + testCase := nerdtest.Setup() + testCase.Require = journaldRequire() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + startTime := time.Now().Format("2006-01-02 15:04:05") + helpers.Ensure("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + data.Labels().Set("startTime", startTime) + data.Labels().Set("fullID", inspect.ID) + waitForJournaldLogs(startTime, fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspect.ID), "foo", "bar") } - if runtime.GOOS == "windows" { - t.Skip("journald log driver is not yet implemented on Windows") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - defer base.Cmd("rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", "journald", "--log-opt", "tag={{.FullID}}", "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - - time.Sleep(3 * time.Second) - inspectedContainer := base.InspectContainer(containerName) - found := 0 - check := func(log poll.LogT) poll.Result { - res := icmd.RunCmd(icmd.Command(journalctl, "--no-pager", "--since", "2 minutes ago", fmt.Sprintf("SYSLOG_IDENTIFIER=%s", inspectedContainer.ID))) - assert.Equal(t, 0, res.ExitCode, res) - if strings.Contains(res.Stdout(), "bar") && strings.Contains(res.Stdout(), "foo") { - found = 1 - return poll.Success() - } - return poll.Continue("reading from journald is not yet finished") + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + journalctl, _ := exec.LookPath("journalctl") + return helpers.Custom(journalctl, "--no-pager", "--since", data.Labels().Get("startTime"), + fmt.Sprintf("SYSLOG_IDENTIFIER=%s", data.Labels().Get("fullID"))) } - poll.WaitOn(t, check, poll.WithDelay(100*time.Microsecond), poll.WithTimeout(20*time.Second)) - assert.Equal(t, 1, found) + + testCase.Expected = journaldExpected() + + testCase.Run(t) } func TestRunWithLogBinary(t *testing.T) { - testutil.RequiresBuild(t) - if runtime.GOOS == "windows" { - t.Skip("buildkit is not enabled on windows, this feature may work on windows.") - } - testutil.DockerIncompatible(t) - t.Parallel() - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) + "-image" - containerName := testutil.Identifier(t) + testCase := nerdtest.Setup() + testCase.Require = require.All( + nerdtest.Build, + require.Not(require.Windows), + require.Not(nerdtest.Docker), + ) var dockerfile = ` FROM ` + testutil.GolangImage + ` as builder @@ -539,312 +596,323 @@ FROM scratch COPY --from=builder /go/src/logger/logger / ` - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tmpDir := t.TempDir() - base.Cmd("build", buildCtx, "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", tmpDir)).AssertOK() - defer base.Cmd("image", "rm", "-f", imageName).AssertOK() - - base.Cmd("container", "rm", "-f", containerName).AssertOK() - base.Cmd("run", "-d", "--log-driver", fmt.Sprintf("binary://%s/logger", tmpDir), "--name", containerName, testutil.CommonImage, - "sh", "-euxc", "echo foo; echo bar").AssertOK() - defer base.Cmd("container", "rm", "-f", containerName).AssertOK() - - inspectedContainer := base.InspectContainer(containerName) - bytes, err := os.ReadFile(filepath.Join(os.TempDir(), fmt.Sprintf("%s_%s.log", inspectedContainer.ID, "stdout"))) - assert.NilError(t, err) - log := string(bytes) - assert.Check(t, strings.Contains(log, "foo")) - assert.Check(t, strings.Contains(log, "bar")) -} + testCase.Setup = func(data test.Data, helpers test.Helpers) { + data.Temp().Save(dockerfile, "Dockerfile") + helpers.Ensure("build", data.Temp().Path(), + "--output", fmt.Sprintf("type=local,src=/go/src/logger/logger,dest=%s", data.Temp().Path())) + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + } -// history: There was a bug that the --add-host items disappear when the another container created. -// This test ensures that it doesn't happen. -// (https://github.com/containerd/nerdctl/issues/2560) -func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("ocihook is not yet supported on Windows") + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + helpers.Anyhow("builder", "prune", "--all", "--force") } - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - hostMapping := "test-add-host:10.0.0.1" - base.Cmd("run", "-d", "--add-host", hostMapping, "--name", containerName, testutil.CommonImage, "sleep", nerdtest.Infinity).AssertOK() - defer base.Cmd("container", "rm", "-f", containerName).Run() + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", + "--log-driver", fmt.Sprintf("binary://%s/logger", data.Temp().Path()), + "--name", data.Identifier(), testutil.CommonImage, + "sh", "-euxc", "echo foo; echo bar") + } - checkEtcHosts := func(stdout string) error { - matcher, err := regexp.Compile(`^10.0.0.1\s+test-add-host$`) - if err != nil { - return err - } - var found bool - sc := bufio.NewScanner(bytes.NewBufferString(stdout)) - for sc.Scan() { - if matcher.Match(sc.Bytes()) { - found = true - } - } - if !found { - return fmt.Errorf("host not found") + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + containerID := strings.TrimSpace(stdout) + logBytes, err := os.ReadFile(filepath.Join(os.TempDir(), + fmt.Sprintf("%s_stdout.log", containerID))) + assert.NilError(t, err) + log := string(logBytes) + assert.Assert(t, strings.Contains(log, "foo")) + assert.Assert(t, strings.Contains(log, "bar")) + }, } - return nil } - base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) - - // run another container - base.Cmd("run", "--rm", testutil.CommonImage).AssertOK() - base.Cmd("exec", containerName, "cat", "/etc/hosts").AssertOutWithFunc(checkEtcHosts) + testCase.Run(t) } -// https://github.com/containerd/nerdctl/issues/2726 -func TestRunRmTime(t *testing.T) { - base := testutil.NewBase(t) - base.Cmd("pull", "--quiet", testutil.CommonImage) - t0 := time.Now() - base.Cmd("run", "--rm", testutil.CommonImage, "true").AssertOK() - t1 := time.Now() - took := t1.Sub(t0) - 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) - } -} +// history: There was a bug that the --add-host items disappear when the another container created. +// This test ensures that it doesn't happen. +// (https://github.com/containerd/nerdctl/issues/2560) +func TestRunAddHostRemainsWhenAnotherContainerCreated(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) -func runAttachStdin(t *testing.T, testStr string, args []string) string { - if runtime.GOOS == "windows" { - t.Skip("run attach test is not yet implemented on Windows") + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("run", "-d", "--add-host", "test-add-host:10.0.0.1", "--name", data.Identifier(), testutil.CommonImage, "sleep", nerdtest.Infinity) + helpers.Ensure("exec", data.Identifier(), "grep", "10.0.0.1.*test-add-host", "/etc/hosts") } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("container", "rm", "-f", data.Identifier()) + } - opts := []func(*testutil.Cmd){ - testutil.WithStdin(strings.NewReader("echo " + testStr + "\nexit\n")), + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + // run another container to verify --add-host entry is not disturbed + helpers.Ensure("run", "--rm", testutil.CommonImage) + return helpers.Command("exec", data.Identifier(), "cat", "/etc/hosts") } - fullArgs := []string{"run", "--rm", "-i"} - fullArgs = append(fullArgs, args...) - fullArgs = append(fullArgs, - "--name", - containerName, - testutil.CommonImage, + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, + expect.Match(regexp.MustCompile(`(?m)^10\.0\.0\.1\s+test-add-host$`)), ) - defer base.Cmd("rm", "-f", containerName).AssertOK() - result := base.Cmd(fullArgs...).CmdOption(opts...).Run() - - return result.Combined() + testCase.Run(t) } -func runAttach(t *testing.T, testStr string, args []string) string { - if runtime.GOOS == "windows" { - t.Skip("run attach test is not yet implemented on Windows") +// https://github.com/containerd/nerdctl/issues/2726 +func TestRunRmTime(t *testing.T) { + testCase := nerdtest.Setup() + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) } - t.Parallel() - base := testutil.NewBase(t) - containerName := testutil.Identifier(t) - - fullArgs := []string{"run"} - fullArgs = append(fullArgs, args...) - fullArgs = append(fullArgs, - "--name", - containerName, - testutil.CommonImage, - "sh", - "-euxc", - "echo "+testStr, - ) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + data.Labels().Set("start", time.Now().Format(time.RFC3339Nano)) + return helpers.Command("run", "--rm", testutil.CommonImage, "true") + } - defer base.Cmd("rm", "-f", containerName).AssertOK() - result := base.Cmd(fullArgs...).Run() + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(stdout string, t tig.T) { + start, _ := time.Parse(time.RFC3339Nano, data.Labels().Get("start")) + took := time.Since(start) + 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 + } + assert.Assert(t, took <= deadline, "expected to have completed in %v, took %v", deadline, took) + }, + } + } - return result.Combined() + testCase.Run(t) } func TestRunAttachFlag(t *testing.T) { + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) - type testCase struct { - name string + type attachTC struct { + description string args []string - testFunc func(t *testing.T, testStr string, args []string) string + useStdin bool + isError bool testStr string expectedOut string dockerOut string } - testCases := []testCase{ + + tcs := []attachTC{ { - name: "AttachFlagStdin", + description: "AttachFlagStdin", args: []string{"-a", "STDIN", "-a", "STDOUT"}, - testFunc: runAttachStdin, + useStdin: true, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, { - name: "AttachFlagStdOut", + description: "AttachFlagStdOut", args: []string{"-a", "STDOUT"}, - testFunc: runAttach, testStr: "foo", expectedOut: "foo", dockerOut: "foo", }, { - name: "AttachFlagMixedValue", + description: "AttachFlagMixedValue", args: []string{"-a", "STDIN", "-a", "invalid-value"}, - testFunc: runAttach, + isError: true, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { - name: "AttachFlagInvalidValue", + description: "AttachFlagInvalidValue", args: []string{"-a", "invalid-stream"}, - testFunc: runAttach, + isError: true, testStr: "foo", expectedOut: "invalid stream specified with -a flag. Valid streams are STDIN, STDOUT, and STDERR", dockerOut: "valid streams are STDIN, STDOUT and STDERR", }, { - name: "AttachFlagCaseInsensitive", + description: "AttachFlagCaseInsensitive", args: []string{"-a", "stdin", "-a", "stdout"}, - testFunc: runAttachStdin, + useStdin: true, testStr: "test-run-stdio", expectedOut: "test-run-stdio", dockerOut: "test-run-stdio", }, } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - actualOut := tc.testFunc(t, tc.testStr, tc.args) - errorMsg := fmt.Sprintf("%s failed;\nExpected: '%s'\nActual: '%s'", tc.name, tc.expectedOut, actualOut) - if nerdtest.IsDocker() { - assert.Equal(t, true, strings.Contains(actualOut, tc.dockerOut), errorMsg) - } else { - assert.Equal(t, true, strings.Contains(actualOut, tc.expectedOut), errorMsg) - } + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + var args []string + if tc.useStdin { + args = append([]string{"run", "--rm", "-i"}, tc.args...) + } else { + args = append([]string{"run"}, tc.args...) + } + args = append(args, "--name", data.Identifier(), testutil.CommonImage) + if !tc.useStdin { + args = append(args, "sh", "-euxc", "echo "+tc.testStr) + } + cmd := helpers.Command(args...) + if tc.useStdin { + cmd.Feed(strings.NewReader("echo " + tc.testStr + "\nexit\n")) + } + return cmd + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + out := tc.expectedOut + if nerdtest.IsDocker() { + out = tc.dockerOut + } + if tc.isError { + return &test.Expected{ + ExitCode: expect.ExitCodeGenericFail, + Errors: []error{errors.New(out)}, + } + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Contains(out), + } + }, }) } + + testCase.Run(t) } func TestRunQuiet(t *testing.T) { - base := testutil.NewBase(t) + testCase := nerdtest.Setup() - teardown := func() { - base.Cmd("rmi", "-f", testutil.CommonImage).Run() + testCase.Setup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", testutil.CommonImage) } - defer teardown() - teardown() - sentinel := "test run quiet" - result := base.Cmd("run", "--rm", "--quiet", testutil.CommonImage, fmt.Sprintf(`echo "%s"`, sentinel)).Run() - assert.Assert(t, strings.Contains(result.Combined(), sentinel)) + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", testutil.CommonImage) + } - wasQuiet := func(output, sentinel string) bool { - return !strings.Contains(output, sentinel) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", "--quiet", testutil.CommonImage, "echo", "test run quiet") } - // Docker and nerdctl image pulls are not 1:1. - if nerdtest.IsDocker() { - sentinel = "Pull complete" - } else { - sentinel = "resolved" + testCase.Expected = func(data test.Data, helpers test.Helpers) *test.Expected { + // Docker and nerdctl image pulls are not 1:1. + pullSentinel := "resolved" + if nerdtest.IsDocker() { + pullSentinel = "Pull complete" + } + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All( + expect.Contains("test run quiet"), + expect.DoesNotContain(pullSentinel), + ), + } } - assert.Assert(t, wasQuiet(result.Combined(), sentinel), "Found %s in container run output", sentinel) + testCase.Run(t) } func TestRunFromOCIArchive(t *testing.T) { - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) + testCase := nerdtest.Setup() + testCase.Require = require.All(nerdtest.Build, require.Not(nerdtest.Docker)) - // Docker does not support running container images from OCI archive. - testutil.DockerIncompatible(t) + const sentinel = "test-nerdctl-run-from-oci-archive" - base := testutil.NewBase(t) - imageName := testutil.Identifier(t) + testCase.Setup = func(data test.Data, helpers test.Helpers) { + tag := fmt.Sprintf("%s:latest", data.Identifier()) + helpers.Anyhow("rmi", "-f", tag) + + dockerfile := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"%s\"]", testutil.CommonImage, sentinel) + data.Temp().Save(dockerfile, "Dockerfile") + tarPath := data.Temp().Path(data.Identifier() + ".tar") + helpers.Ensure("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), data.Temp().Path()) + data.Labels().Set("tag", tag) + data.Labels().Set("tarPath", tarPath) + } - teardown := func() { - base.Cmd("rmi", "-f", imageName).Run() + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rmi", "-f", data.Labels().Get("tag")) + helpers.Anyhow("builder", "prune", "--all", "--force") } - defer teardown() - teardown() - const sentinel = "test-nerdctl-run-from-oci-archive" - dockerfile := fmt.Sprintf(`FROM %s - CMD ["echo", "%s"]`, testutil.CommonImage, sentinel) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", fmt.Sprintf("oci-archive://%s", data.Labels().Get("tarPath"))) + } - buildCtx := helpers.CreateBuildContext(t, dockerfile) - tag := fmt.Sprintf("%s:latest", imageName) - tarPath := fmt.Sprintf("%s/%s.tar", buildCtx, imageName) + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(sentinel)) - base.Cmd("build", "--tag", tag, fmt.Sprintf("--output=type=oci,dest=%s", tarPath), buildCtx).AssertOK() - base.Cmd("run", "--rm", fmt.Sprintf("oci-archive://%s", tarPath)).AssertOutContainsAll(tag, sentinel) + testCase.Run(t) } func TestRunDomainname(t *testing.T) { - t.Parallel() - - if runtime.GOOS == "windows" { - t.Skip("run --hostname not implemented on Windows yet") - } + testCase := nerdtest.Setup() + testCase.Require = require.Not(require.Windows) - testCases := []struct { - name string + type domainnameTC struct { + description string hostname string domainname string - Cmd string - CmdFlag string + cmd string + cmdFlag string expectedOut string - }{ + } + + tcs := []domainnameTC{ { - name: "Check domain name", + description: "Check domain name", hostname: "foobar", domainname: "example.com", - Cmd: "hostname", - CmdFlag: "-d", + cmd: "hostname", + cmdFlag: "-d", expectedOut: "example.com", }, { - name: "check fqdn", + description: "check fqdn", hostname: "foobar", domainname: "example.com", - Cmd: "hostname", - CmdFlag: "-f", + cmd: "hostname", + cmdFlag: "-f", expectedOut: "foobar.example.com", }, } - for _, tc := range testCases { - tc := tc // capture range variable - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - base := testutil.NewBase(t) - - base.Cmd("run", - "--rm", - "--hostname", tc.hostname, - "--domainname", tc.domainname, - testutil.CommonImage, - tc.Cmd, - tc.CmdFlag, - ).AssertOutContains(tc.expectedOut) + for _, tc := range tcs { + testCase.SubTests = append(testCase.SubTests, &test.Case{ + Description: tc.description, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "--rm", + "--hostname", tc.hostname, + "--domainname", tc.domainname, + testutil.CommonImage, + tc.cmd, tc.cmdFlag, + ) + }, + Expected: test.Expects(expect.ExitCodeSuccess, nil, expect.Contains(tc.expectedOut)), }) } + + testCase.Run(t) } func TestRunHealthcheckFlags(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("healthcheck tests are skipped in rootless environment") - } testCase := nerdtest.Setup() + testCase.Require = require.Not(nerdtest.Rootless) testCases := []struct { name string @@ -940,8 +1008,6 @@ func TestRunHealthcheckFlags(t *testing.T) { } for _, tc := range testCases { - tc := tc - testCase.SubTests = append(testCase.SubTests, &test.Case{ Description: tc.name, Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { @@ -993,72 +1059,66 @@ func TestRunHealthcheckFlags(t *testing.T) { } func TestRunHealthcheckFromImage(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("healthcheck tests are skipped in rootless environment") - } - nerdtest.Setup() - dockerfile := fmt.Sprintf(`FROM %s HEALTHCHECK --interval=30s --timeout=10s CMD wget -q --spider http://localhost:8080 || exit 1 `, testutil.CommonImage) - 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()) + testCase := nerdtest.Setup() + testCase.Require = require.All(nerdtest.Build, require.Not(nerdtest.Rootless)) + testCase.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()) + } + testCase.SubTests = []*test.Case{ + { + Description: "merge_with_image", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("run", "-d", "--name", data.Identifier(), + "--health-retries=5", + "--health-interval=45s", + data.Labels().Get("image")) + }, + Expected: func(data test.Data, helpers test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + hc := inspect.Config.Healthcheck + assert.Assert(t, hc != nil, "expected healthcheck config to be present") + assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "wget -q --spider http://localhost:8080 || exit 1"}) + assert.Equal(t, 5, hc.Retries) // From CLI flags + assert.Equal(t, 45*time.Second, hc.Interval) // From CLI flags + assert.Equal(t, 10*time.Second, hc.Timeout) // From Dockerfile + }), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) + }, }, - SubTests: []*test.Case{ - { - Description: "merge_with_image", - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command("run", "-d", "--name", data.Identifier(), - "--health-retries=5", - "--health-interval=45s", - data.Labels().Get("image")) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t tig.T) { - inspect := nerdtest.InspectContainer(helpers, data.Identifier()) - hc := inspect.Config.Healthcheck - assert.Assert(t, hc != nil, "expected healthcheck config to be present") - assert.DeepEqual(t, hc.Test, []string{"CMD-SHELL", "wget -q --spider http://localhost:8080 || exit 1"}) - assert.Equal(t, 5, hc.Retries) // From CLI flags - assert.Equal(t, 45*time.Second, hc.Interval) // From CLI flags - assert.Equal(t, 10*time.Second, hc.Timeout) // From Dockerfile - }), - } - }, - Cleanup: func(data test.Data, helpers test.Helpers) { - helpers.Anyhow("rm", "-f", data.Identifier()) - }, + { + Description: "Disable image health checks via runtime flag", + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command( + "run", "-d", "--name", data.Identifier(), + "--no-healthcheck", + data.Labels().Get("image"), + ) }, - { - Description: "Disable image health checks via runtime flag", - Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { - return helpers.Command( - "run", "-d", "--name", data.Identifier(), - "--no-healthcheck", - data.Labels().Get("image"), - ) - }, - Expected: func(data test.Data, helpers test.Helpers) *test.Expected { - return &test.Expected{ - ExitCode: expect.ExitCodeSuccess, - Output: expect.All(func(stdout string, t tig.T) { - inspect := nerdtest.InspectContainer(helpers, data.Identifier()) - hc := inspect.Config.Healthcheck - assert.Assert(t, hc != nil, "expected healthcheck config to be present") - assert.DeepEqual(t, hc.Test, []string{"NONE"}) - }), - } - }, - 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: expect.ExitCodeSuccess, + Output: expect.All(func(stdout string, t tig.T) { + inspect := nerdtest.InspectContainer(helpers, data.Identifier()) + hc := inspect.Config.Healthcheck + assert.Assert(t, hc != nil, "expected healthcheck config to be present") + assert.DeepEqual(t, hc.Test, []string{"NONE"}) + }), + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + helpers.Anyhow("rm", "-f", data.Identifier()) }, }, } @@ -1080,20 +1140,18 @@ func countFIFOFiles(root string) (int, error) { return count, err } func TestCleanupFIFOs(t *testing.T) { - if rootlessutil.IsRootless() { - t.Skip("/run/containerd/fifo/ doesn't exist on rootless") - } - if runtime.GOOS == "windows" { - t.Skip("test is not compatible with windows") - } - testutil.DockerIncompatible(t) testCase := nerdtest.Setup() + testCase.Require = require.All( + require.Not(require.Windows), + require.Not(nerdtest.Docker), + require.Not(nerdtest.Rootless), // /run/containerd/fifo/ doesn't exist on rootless + ) testCase.NoParallel = true testCase.Setup = func(data test.Data, helpers test.Helpers) { cmd := helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, }) oldNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) @@ -1101,7 +1159,7 @@ func TestCleanupFIFOs(t *testing.T) { cmd = helpers.Command("run", "-it", "--rm", testutil.CommonImage, "date") cmd.WithPseudoTTY() cmd.Run(&test.Expected{ - ExitCode: 0, + ExitCode: expect.ExitCodeSuccess, }) newNumFifos, err := countFIFOFiles("/run/containerd/fifo/") assert.NilError(t, err) diff --git a/cmd/nerdctl/container/multi_platform_linux_test.go b/cmd/nerdctl/container/multi_platform_linux_test.go index 01ae208528c..0f7d326c398 100644 --- a/cmd/nerdctl/container/multi_platform_linux_test.go +++ b/cmd/nerdctl/container/multi_platform_linux_test.go @@ -22,17 +22,39 @@ import ( "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/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/testutil" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" "github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil" - "github.com/containerd/nerdctl/v2/pkg/testutil/testregistry" ) -func testMultiPlatformRun(base *testutil.Base, alpineImage string) { - t := base.T - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") +// randomPort asks the registry helpers to acquire a free port automatically. +const randomPort = 0 + +// requireMultiPlatformExec skips the test when the host cannot execute +// linux/amd64, linux/arm64 and linux/arm/v7 images (e.g. no binfmt_misc). +var requireMultiPlatformExec = &test.Requirement{ + Check: func(_ test.Data, _ test.Helpers) (bool, string) { + ok, err := platformutil.CanExecProbably("linux/amd64", "linux/arm64", "linux/arm/v7") + if !ok { + msg := "requires multi-platform exec support (linux/amd64, linux/arm64, linux/arm/v7)" + if err != nil { + msg += ": " + err.Error() + } + return false, msg + } + return true, "" + }, +} + +// assertMultiPlatformRun runs uname -m inside image on each platform and +// asserts the expected machine type string. +func assertMultiPlatformRun(helpers test.Helpers, image string) { testCases := map[string]string{ "amd64": "x86_64", "arm64": "aarch64", @@ -41,92 +63,174 @@ func testMultiPlatformRun(base *testutil.Base, alpineImage string) { "linux/arm/v7": "armv7l", } for plat, expectedUnameM := range testCases { - t.Logf("Testing %q (%q)", plat, expectedUnameM) - cmd := base.Cmd("run", "--rm", "--platform="+plat, alpineImage, "uname", "-m") - cmd.AssertOutExactly(expectedUnameM + "\n") + helpers.T().Log(fmt.Sprintf("Testing platform %q (%q)", plat, expectedUnameM)) + helpers.Command("run", "--rm", "--platform="+plat, image, "uname", "-m"). + Run(&test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: expect.Equals(expectedUnameM + "\n"), + }) } } func TestMultiPlatformRun(t *testing.T) { - base := testutil.NewBase(t) - testMultiPlatformRun(base, testutil.AlpineImage) + testCase := nerdtest.Setup() + + testCase.Require = requireMultiPlatformExec + + testCase.Setup = func(_ test.Data, helpers test.Helpers) { + assertMultiPlatformRun(helpers, testutil.AlpineImage) + } + + testCase.Run(t) } func TestMultiPlatformBuildPush(t *testing.T) { - testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -RUN echo dummy - `, testutil.AlpineImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() - testMultiPlatformRun(base, imageName) - base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() + testCase := nerdtest.Setup() + + testCase.Require = require.All( + // non-buildx `docker build` lacks multi-platform support; `docker push` lacks --platform + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", imageName) + + dockerfile := fmt.Sprintf("FROM %s\nRUN echo dummy\n", testutil.AlpineImage) + buildCtx := data.Temp().Dir() + data.Temp().Save(dockerfile, "Dockerfile") + + helpers.Ensure("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + imageName := data.Labels().Get("image") + assertMultiPlatformRun(helpers, imageName) + return helpers.Command("push", "--platform=amd64,arm64,linux/arm/v7", imageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } -// TestMultiPlatformBuildPushNoRun tests if the push succeeds in a situation where nerdctl builds -// a Dockerfile without RUN, COPY, etc commands. In such situation, BuildKit doesn't download the base image -// so nerdctl needs to ensure these blobs to be locally available. func TestMultiPlatformBuildPushNoRun(t *testing.T) { - testutil.DockerIncompatible(t) // non-buildx version of `docker build` lacks multi-platform. Also, `docker push` lacks --platform. - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", imageName).Run() - - dockerfile := fmt.Sprintf(`FROM %s -CMD echo dummy - `, testutil.AlpineImage) - - buildCtx := helpers.CreateBuildContext(t, dockerfile) - - base.Cmd("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx).AssertOK() - testMultiPlatformRun(base, imageName) - base.Cmd("push", "--platform=amd64,arm64,linux/arm/v7", imageName).AssertOK() + testCase := nerdtest.Setup() + + testCase.Require = require.All( + // non-buildx `docker build` lacks multi-platform support; `docker push` lacks --platform + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + imageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", imageName) + + dockerfile := fmt.Sprintf("FROM %s\nCMD echo dummy\n", testutil.AlpineImage) + buildCtx := data.Temp().Dir() + data.Temp().Save(dockerfile, "Dockerfile") + + helpers.Ensure("build", "-t", imageName, "--platform=amd64,arm64,linux/arm/v7", buildCtx) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + helpers.Anyhow("builder", "prune", "--all", "--force") + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + imageName := data.Labels().Get("image") + assertMultiPlatformRun(helpers, imageName) + return helpers.Command("push", "--platform=amd64,arm64,linux/arm/v7", imageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestMultiPlatformPullPushAllPlatforms(t *testing.T) { - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - tID := testutil.Identifier(t) - reg := testregistry.NewWithNoAuth(base, 0, false) - defer reg.Cleanup(nil) - - pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, tID) - defer base.Cmd("rmi", pushImageName).Run() - - base.Cmd("pull", "--quiet", "--all-platforms", testutil.AlpineImage).AssertOK() - base.Cmd("tag", testutil.AlpineImage, pushImageName).AssertOK() - base.Cmd("push", "--all-platforms", pushImageName).AssertOK() - testMultiPlatformRun(base, pushImageName) + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Docker), + requireMultiPlatformExec, + nerdtest.Registry, + ) + + var reg *registry.Server + + testCase.Setup = func(data test.Data, helpers test.Helpers) { + reg = nerdtest.RegistryWithNoAuth(data, helpers, randomPort, false) + reg.Setup(data, helpers) + pushImageName := fmt.Sprintf("localhost:%d/%s:latest", reg.Port, data.Identifier()) + data.Labels().Set("image", pushImageName) + helpers.Ensure("pull", "--quiet", "--all-platforms", testutil.AlpineImage) + helpers.Ensure("tag", testutil.AlpineImage, pushImageName) + } + + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if img := data.Labels().Get("image"); img != "" { + helpers.Anyhow("rmi", img) + } + if reg != nil { + reg.Cleanup(data, helpers) + } + } + + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + pushImageName := data.Labels().Get("image") + helpers.Ensure("push", "--all-platforms", pushImageName) + assertMultiPlatformRun(helpers, pushImageName) + return helpers.Command("inspect", "--type=image", pushImageName) + } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } func TestMultiPlatformComposeUpBuild(t *testing.T) { - testutil.DockerIncompatible(t) - testutil.RequiresBuild(t) - testutil.RegisterBuildCacheCleanup(t) - testutil.RequireExecPlatform(t, "linux/amd64", "linux/arm64", "linux/arm/v7") - base := testutil.NewBase(t) + testCase := nerdtest.Setup() + + testCase.Require = require.All( + require.Not(nerdtest.Docker), + nerdtest.Build, + requireMultiPlatformExec, + ) - const dockerComposeYAML = ` + testCase.Setup = func(data test.Data, helpers test.Helpers) { + dockerfile := fmt.Sprintf("FROM %s\nRUN uname -m > /usr/share/nginx/html/index.html\n", testutil.NginxAlpineImage) + composeYAML := ` services: svc0: build: . @@ -144,30 +248,49 @@ services: ports: - 8082:80 ` - dockerfile := fmt.Sprintf(`FROM %s -RUN uname -m > /usr/share/nginx/html/index.html -`, testutil.NginxAlpineImage) + buildCtx := data.Temp().Dir() + composePath := data.Temp().Save(composeYAML, "compose.yaml") + _ = buildCtx + data.Temp().Save(dockerfile, "Dockerfile") + data.Labels().Set("composePath", composePath) - comp := testutil.NewComposeDir(t, dockerComposeYAML) - defer comp.CleanUp() - - comp.WriteFile("Dockerfile", dockerfile) - - base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d", "--build").AssertOK() - defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run() + helpers.Ensure("compose", "-f", composePath, "up", "-d", "--build") + } - testCases := map[string]string{ - "http://127.0.0.1:8080": "x86_64", - "http://127.0.0.1:8081": "aarch64", - "http://127.0.0.1:8082": "armv7l", + testCase.Cleanup = func(data test.Data, helpers test.Helpers) { + if cp := data.Labels().Get("composePath"); cp != "" { + helpers.Anyhow("compose", "-f", cp, "down", "-v") + } + helpers.Anyhow("builder", "prune", "--all", "--force") } - for testURL, expectedIndexHTML := range testCases { - resp, err := nettestutil.HTTPGet(testURL, 5, false) - assert.NilError(t, err) - respBody, err := io.ReadAll(resp.Body) - assert.NilError(t, err) - t.Logf("respBody=%q", respBody) - assert.Assert(t, strings.Contains(string(respBody), expectedIndexHTML)) + testCase.Command = func(data test.Data, helpers test.Helpers) test.TestableCommand { + urlExpected := map[string]string{ + "http://127.0.0.1:8080": "x86_64", + "http://127.0.0.1:8081": "aarch64", + "http://127.0.0.1:8082": "armv7l", + } + for url, expected := range urlExpected { + resp, err := nettestutil.HTTPGet(url, 5, false) + if err != nil { + helpers.T().Log(fmt.Sprintf("GET %s: %v", url, err)) + helpers.T().FailNow() + } + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + helpers.T().Log(fmt.Sprintf("reading body from %s: %v", url, err)) + helpers.T().FailNow() + } + if !strings.Contains(string(body), expected) { + helpers.T().Log(fmt.Sprintf("expected %q in body from %s, got %q", expected, url, string(body))) + helpers.T().Fail() + } + } + return helpers.Command("compose", "-f", data.Labels().Get("composePath"), "ps") } + + testCase.Expected = test.Expects(expect.ExitCodeSuccess, nil, nil) + + testCase.Run(t) } diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index c253a2274e4..105dd0ea161 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -88,6 +88,7 @@ func convertCommand() *cobra.Command { cmd.Flags().Bool("overlaybd", false, "Convert tar.gz layers to overlaybd layers") cmd.Flags().String("overlaybd-fs-type", "ext4", "Filesystem type for overlaybd") cmd.Flags().String("overlaybd-dbstr", "", "Database config string for overlaybd") + cmd.Flags().Int("overlaybd-vsize", 64, "Virtual block device size in GB for overlaybd") // #endregion // #region soci flags @@ -226,6 +227,10 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { if err != nil { return types.ImageConvertOptions{}, err } + overlaybdVsize, err := cmd.Flags().GetInt("overlaybd-vsize") + if err != nil { + return types.ImageConvertOptions{}, err + } // #endregion // #region soci flags @@ -307,6 +312,7 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { Overlaybd: overlaybd, OverlayFsType: overlaybdFsType, OverlaydbDBStr: overlaybdDbstr, + OverlaybdVsize: overlaybdVsize, }, SociConvertOptions: types.SociConvertOptions{ Soci: soci, diff --git a/cmd/nerdctl/image/image_convert_test.go b/cmd/nerdctl/image/image_convert_test.go new file mode 100644 index 00000000000..c9ab36ff2e5 --- /dev/null +++ b/cmd/nerdctl/image/image_convert_test.go @@ -0,0 +1,86 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "testing" + + "github.com/spf13/cobra" + "gotest.tools/v3/assert" +) + +func TestConvertOptionsOverlaybdVsize(t *testing.T) { + testCases := []struct { + name string + flags map[string]string + want int + }{ + { + name: "default", + want: 64, + }, + { + name: "explicit value", + flags: map[string]string{ + "overlaybd-vsize": "128", + }, + want: 128, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + cmd := convertCommand() + addRootFlagsForConvertOptionsTest(t, cmd) + assert.NilError(t, cmd.Flags().Set("overlaybd", "true")) + for name, value := range tc.flags { + assert.NilError(t, cmd.Flags().Set(name, value)) + } + + got, err := convertOptions(cmd) + assert.NilError(t, err) + assert.Equal(t, got.OverlaybdVsize, tc.want) + }) + } +} + +func addRootFlagsForConvertOptionsTest(t *testing.T, cmd *cobra.Command) { + t.Helper() + + flags := cmd.Flags() + flags.Bool("debug", false, "") + flags.Bool("debug-full", false, "") + flags.String("address", "", "") + flags.String("namespace", "default", "") + flags.String("snapshotter", "", "") + flags.String("cni-path", "", "") + flags.String("cni-netconfpath", "", "") + flags.String("data-root", t.TempDir(), "") + flags.String("cgroup-manager", "", "") + flags.Bool("insecure-registry", false, "") + flags.StringSlice("hosts-dir", nil, "") + flags.Bool("experimental", false, "") + flags.String("host-gateway-ip", "", "") + flags.String("bridge-ip", "", "") + flags.Bool("kube-hide-dupe", false, "") + flags.StringSlice("cdi-spec-dirs", nil, "") + flags.StringSlice("global-dns", nil, "") + flags.StringSlice("global-dns-opts", nil, "") + flags.StringSlice("global-dns-search", nil, "") + flags.Bool("selinux-enabled", false, "") +} diff --git a/cmd/nerdctl/image/image_history.go b/cmd/nerdctl/image/image_history.go index 79384701f9e..a6f20416f58 100644 --- a/cmd/nerdctl/image/image_history.go +++ b/cmd/nerdctl/image/image_history.go @@ -63,7 +63,7 @@ func addHistoryFlags(cmd *cobra.Command) { return []string{"json"}, cobra.ShellCompDirectiveNoFileComp }) cmd.Flags().BoolP("quiet", "q", false, "Only show numeric IDs") - cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format (default true)") + cmd.Flags().BoolP("human", "H", true, "Print sizes and dates in human readable format") cmd.Flags().Bool("no-trunc", false, "Don't truncate output") } diff --git a/cmd/nerdctl/image/image_history_help_test.go b/cmd/nerdctl/image/image_history_help_test.go new file mode 100644 index 00000000000..f1b1bec4f2a --- /dev/null +++ b/cmd/nerdctl/image/image_history_help_test.go @@ -0,0 +1,40 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package image + +import ( + "bytes" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestHistoryHelpDoesNotDuplicateDefaults(t *testing.T) { + cmd := HistoryCommand() + var stdout bytes.Buffer + cmd.SetOut(&stdout) + cmd.SetErr(&stdout) + cmd.SetArgs([]string{"--help"}) + + err := cmd.Execute() + assert.NilError(t, err) + + help := stdout.String() + assert.Assert(t, strings.Contains(help, "Print sizes and dates in human readable format (default true)")) + assert.Assert(t, !strings.Contains(help, "Print sizes and dates in human readable format (default true) (default true)")) +} diff --git a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go index de1dc16d239..e770d9f8422 100644 --- a/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go +++ b/cmd/nerdctl/ipfs/ipfs_kubo_linux_test.go @@ -43,6 +43,7 @@ func TestIPFSAddrWithKubo(t *testing.T) { require.Not(nerdtest.Docker), nerdtest.Registry, nerdtest.Private, + nerdtest.IsFlaky("https://github.com/containerd/nerdctl/issues/4838"), ) testCase.Setup = func(data test.Data, helpers test.Helpers) { diff --git a/cmd/nerdctl/login/login_linux_test.go b/cmd/nerdctl/login/login_linux_test.go index 55544b33ad8..90834490508 100644 --- a/cmd/nerdctl/login/login_linux_test.go +++ b/cmd/nerdctl/login/login_linux_test.go @@ -23,21 +23,23 @@ package login import ( "fmt" "net" - "os" "strconv" "testing" - "gotest.tools/v3/icmd" - + "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/utils" + "github.com/containerd/nerdctl/mod/tigron/utils/testca" "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" + "github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest/registry" ) +// randomPort tells the registry helpers to acquire a free port automatically. +const randomPort = 0 + type Client struct { args []string configPath string @@ -68,106 +70,118 @@ func (ag *Client) WithConfigPath(value string) *Client { return ag } -func (ag *Client) GetConfigPath() string { - return ag.configPath -} - -func (ag *Client) Run(base *testutil.Base, host string) *testutil.Cmd { +func (ag *Client) Cmd(helpers test.Helpers, host string) test.TestableCommand { if ag.configPath == "" { - ag.configPath, _ = os.MkdirTemp(base.T.TempDir(), "docker-config") + ag.configPath = helpers.T().TempDir() } args := []string{"login"} if !nerdtest.IsDocker() { args = append(args, "--debug-full") } args = append(args, ag.args...) - icmdCmd := icmd.Command(base.Binary, append(base.Args, append(args, host)...)...) - icmdCmd.Env = append(base.Env, "HOME="+os.Getenv("HOME"), "DOCKER_CONFIG="+ag.configPath) - - return &testutil.Cmd{ - Cmd: icmdCmd, - Base: base, - } + args = append(args, host) + cmd := helpers.Command(args...) + cmd.Setenv("DOCKER_CONFIG", ag.configPath) + return cmd } func TestLoginPersistence(t *testing.T) { - base := testutil.NewBase(t) - t.Parallel() - - // Retrieve from the store - testCases := []struct { - auth string - }{ - { - "basic", - }, - { - "token", + nerdtest.Setup() + + var basicReg *registry.Server + var tokenReg *registry.Server + var tokenAS *registry.TokenAuthServer + + testCase := &test.Case{ + Require: require.All( + require.Linux, + nerdtest.Registry, + ), + SubTests: []*test.Case{ + { + Description: "basic", + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + basicReg = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, randomPort, false) + basicReg.Setup(data, helpers) + + host := fmt.Sprintf("localhost:%d", basicReg.Port) + configPath := helpers.T().TempDir() + + (&Client{configPath: configPath}). + WithCredentials(username, password). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + WithCredentials("invalid", "invalid"). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if basicReg != nil { + basicReg.Cleanup(data, helpers) + } + }, + }, + { + Description: "token", + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + // Use HTTP registry (nil CA) so localhost is trusted without explicit hosts-dir, + // matching the original test behaviour. The auth server still uses a CA for JWT + // signing even without TLS on the auth server itself. + rca := testca.NewX509(data, helpers) + tokenAS = registry.NewCesantaAuthServer(data, helpers, rca, randomPort, username, password, false) + tokenAS.Setup(data, helpers) + tokenReg = registry.NewDockerRegistry(data, helpers, nil, randomPort, tokenAS.Auth) + tokenReg.Setup(data, helpers) + + host := fmt.Sprintf("localhost:%d", tokenReg.Port) + configPath := helpers.T().TempDir() + + (&Client{configPath: configPath}). + WithCredentials(username, password). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + + (&Client{configPath: configPath}). + WithCredentials("invalid", "invalid"). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{configPath: configPath}). + Cmd(helpers, host). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if tokenReg != nil { + tokenReg.Cleanup(data, helpers) + } + if tokenAS != nil { + tokenAS.Cleanup(data, helpers) + } + }, + }, }, } - - for _, tc := range testCases { - tc := tc - t.Run(fmt.Sprintf("Server %s", tc.auth), func(t *testing.T) { - t.Parallel() - - username := utils.RandomStringBase64(30) + "∞" - password := utils.RandomStringBase64(30) + ":∞" - - // Add the requested authentication - var auth testregistry.Auth - var dependentCleanup func(error) - - auth = &testregistry.NoAuth{} - if tc.auth == "basic" { - auth = &testregistry.BasicAuth{ - Username: username, - Password: password, - } - } else if tc.auth == "token" { - authCa := testca.New(base.T) - as := testregistry.NewAuthServer(base, authCa, 0, username, password, false) - auth = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, - } - dependentCleanup = as.Cleanup - } - - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, auth, dependentCleanup) - - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) - - // First, login successfully - c := (&Client{}). - WithCredentials(username, password) - - c.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - - // Now, log in successfully without passing any explicit credentials - nc := (&Client{}). - WithConfigPath(c.GetConfigPath()) - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - - // Now fail while using invalid credentials - nc.WithCredentials("invalid", "invalid"). - Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertFail() - - // And login again without, reverting to the last saved good state - nc = (&Client{}). - WithConfigPath(c.GetConfigPath()) - - nc.Run(base, fmt.Sprintf("localhost:%d", reg.Port)). - AssertOK() - }) - } + testCase.Run(t) } /* @@ -176,7 +190,7 @@ func TestAgainstNoAuth(t *testing.T) { t.Parallel() // Start the registry with the requested options - reg := testregistry.NewRegistry(base, nil, 0, &testregistry.NoAuth{}, nil) + reg := testregistry.NewRegistry(base, nil, randomPort, &testregistry.NoAuth{}, nil) // Register registry cleanup t.Cleanup(func() { @@ -202,10 +216,8 @@ func TestAgainstNoAuth(t *testing.T) { func TestLoginAgainstVariants(t *testing.T) { // Skip docker, because Docker doesn't have `--hosts-dir` nor `insecure-registry` option // This will test access to a wide variety of servers, with or without TLS, with basic or token authentication - testutil.DockerIncompatible(t) - base := testutil.NewBase(t) - t.Parallel() + nerdtest.Setup() testCases := []struct { port int @@ -213,238 +225,145 @@ func TestLoginAgainstVariants(t *testing.T) { auth string }{ // Basic auth, no TLS - { - 80, - false, - "basic", - }, - { - 443, - false, - "basic", - }, - { - 0, - false, - "basic", - }, + {80, false, "basic"}, + {443, false, "basic"}, + {0, false, "basic"}, // Token auth, no TLS - { - 80, - false, - "token", - }, - { - 443, - false, - "token", - }, - { - 0, - false, - "token", - }, + {80, false, "token"}, + {443, false, "token"}, + {0, false, "token"}, // Basic auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite - { - 80, - true, - "basic", - }, + {80, true, "basic"}, */ - { - 443, - true, - "basic", - }, - { - 0, - true, - "basic", - }, + {443, true, "basic"}, + {0, true, "basic"}, // Token auth, with TLS /* // This is not working currently, unless we would force a server https:// in hosts // To be fixed with login rewrite - { - 80, - true, - "token", - }, + {80, true, "token"}, */ - { - 443, - true, - "token", - }, - { - 0, - true, - "token", - }, + {443, true, "token"}, + {0, true, "token"}, } - // Iterate through all cases, that will present a variety of port (80, 443, random), TLS (yes or no), and authentication (basic, token) type combinations + var subtests []*test.Case for _, tc := range testCases { - port := tc.port - tls := tc.tls - auth := tc.auth - - t.Run(fmt.Sprintf("Login against `tls: %t port: %d auth: %s`", tls, port, auth), func(t *testing.T) { - // Tests with fixed ports should not be parallelized (although the port locking mechanism will prevent conflicts) - // as their children tests are parallelized, and this might deadlock given the way `Parallel` works - if port == 0 { - t.Parallel() - } - - // Generate credentials that are specific to each registry, so that we never cross hit another one - username := utils.RandomStringBase64(30) + "∞" - password := utils.RandomStringBase64(30) + ":∞" - - // Get a CA if we want TLS - var ca *testca.CA - if tls { - ca = testca.New(base.T) - } - - // Add the requested authenticator - var authenticator testregistry.Auth - var dependentCleanup func(error) - - authenticator = &testregistry.NoAuth{} - if auth == "basic" { - authenticator = &testregistry.BasicAuth{ - Username: username, - Password: password, + tc := tc + + var reg *registry.Server + var tokenAuthServer *registry.TokenAuthServer + + subtests = append(subtests, &test.Case{ + Description: fmt.Sprintf("tls:%t port:%d auth:%s", tc.tls, tc.port, tc.auth), + // Fixed-port cases must not run in parallel: children are parallelised, + // and mixing Parallel levels can deadlock in Go's test runner. + NoParallel: tc.port != 0, + Setup: func(data test.Data, helpers test.Helpers) { + username := utils.RandomStringBase64(30) + "∞" + password := utils.RandomStringBase64(30) + ":∞" + + switch { + case tc.auth == "basic": + reg = nerdtest.RegistryWithBasicAuth(data, helpers, username, password, tc.port, tc.tls) + reg.Setup(data, helpers) + case tc.auth == "token" && tc.tls: + reg, tokenAuthServer = nerdtest.RegistryWithTokenAuth(data, helpers, username, password, tc.port, tc.tls) + tokenAuthServer.Setup(data, helpers) + reg.Setup(data, helpers) + default: // token auth, no TLS: HTTP registry + HTTP auth server (CA used only for JWT) + rca := testca.NewX509(data, helpers) + tokenAuthServer = registry.NewCesantaAuthServer(data, helpers, rca, randomPort, username, password, false) + tokenAuthServer.Setup(data, helpers) + reg = registry.NewDockerRegistry(data, helpers, nil, tc.port, tokenAuthServer.Auth) + reg.Setup(data, helpers) } - } else if auth == "token" { - authCa := ca - // We could be on !tls, meaning no ca - but we still need a CA to sign jwt tokens - if authCa == nil { - authCa = testca.New(base.T) + + regHosts := []string{ + net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)), + net.JoinHostPort("localhost", strconv.Itoa(reg.Port)), + net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)), + // TODO: ipv6 } - as := testregistry.NewAuthServer(base, authCa, 0, username, password, tls) - authenticator = &testregistry.TokenAuth{ - Address: as.Scheme + "://" + net.JoinHostPort(as.IP.String(), strconv.Itoa(as.Port)), - CertPath: as.CertPath, + if reg.Port == 443 { + regHosts = append(regHosts, + reg.IP.String(), + "localhost", + "127.0.0.1", + // TODO: ipv6 + ) } - dependentCleanup = as.Cleanup - } - - // Start the registry with the requested options - reg := testregistry.NewRegistry(base, ca, port, authenticator, dependentCleanup) - - // Register registry cleanup - t.Cleanup(func() { - reg.Cleanup(nil) - }) - - // Any registry is reachable through its ip+port, and localhost variants - regHosts := []string{ - net.JoinHostPort(reg.IP.String(), strconv.Itoa(reg.Port)), - net.JoinHostPort("localhost", strconv.Itoa(reg.Port)), - net.JoinHostPort("127.0.0.1", strconv.Itoa(reg.Port)), - // TODO: ipv6 - // net.JoinHostPort("::1", strconv.Itoa(reg.Port)), - } - - // Registries that use port 443 also allow access without specifying a port - if reg.Port == 443 { - regHosts = append(regHosts, reg.IP.String()) - regHosts = append(regHosts, "localhost") - regHosts = append(regHosts, "127.0.0.1") - // TODO: ipv6 - // regHosts = append(regHosts, "::1") - } - - // Iterate through these hosts access points, and create a test per-variant - for _, value := range regHosts { - regHost := value - t.Run(regHost, func(t *testing.T) { - t.Parallel() - - // 1. test with valid credentials but no access to the CA - t.Run("1. valid credentials (no CA) ", func(t *testing.T) { - t.Parallel() - c := (&Client{}). - WithCredentials(username, password) - - rl, _ := dockerconfigresolver.Parse(regHost) - // a. Insecure flag not being set - // TODO: remove specialization when we fix the localhost mess - if rl.IsLocalhost() && !tls { - c.Run(base, regHost). - AssertOK() - } else { - c.Run(base, regHost). - AssertFail() - } - - // b. Insecure flag set to false - // TODO: remove specialization when we fix the localhost mess - if !rl.IsLocalhost() { - (&Client{}). - WithCredentials(username, password). - WithInsecure(false). - Run(base, regHost). - AssertFail() - } + for _, regHost := range regHosts { + rl, _ := dockerconfigresolver.Parse(regHost) - // c. Insecure flag set to true - // TODO: remove specialization when we fix the localhost mess - if !rl.IsLocalhost() || !tls { - (&Client{}). - WithCredentials(username, password). - WithInsecure(true). - Run(base, regHost). - AssertOK() - } - }) + // 1. valid credentials (no CA) + // a. Insecure flag not being set + // TODO: remove specialization when we fix the localhost mess + if rl.IsLocalhost() && !tc.tls { + (&Client{}). + WithCredentials(username, password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } else { + (&Client{}). + WithCredentials(username, password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } - // 2. test with valid credentials AND access to the CA - t.Run("2. valid credentials (with access to server CA)", func(t *testing.T) { - t.Parallel() + // b. Insecure flag set to false + // TODO: remove specialization when we fix the localhost mess + if !rl.IsLocalhost() { + (&Client{}). + WithCredentials(username, password). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } - rl, _ := dockerconfigresolver.Parse(regHost) + // c. Insecure flag set to true + // TODO: remove specialization when we fix the localhost mess + if !rl.IsLocalhost() || !tc.tls { + (&Client{}). + WithCredentials(username, password). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } + // 2. valid credentials (with access to server CA) + { // a. Insecure flag not being set c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir) - if tls || rl.IsLocalhost() { - c.Run(base, regHost). - AssertOK() + if tc.tls || rl.IsLocalhost() { + c.Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } else { - c.Run(base, regHost). - AssertFail() + c.Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) } // b. Insecure flag set to false - if tls { - c.WithInsecure(false). - Run(base, regHost). - AssertOK() + if tc.tls { + c.WithInsecure(false).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } else { // TODO: remove specialization when we fix the localhost mess if !rl.IsLocalhost() { - c.WithInsecure(false). - Run(base, regHost). - AssertFail() + c.WithInsecure(false).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) } } // c. Insecure flag set to true - c.WithInsecure(true). - Run(base, regHost). - AssertOK() - }) + c.WithInsecure(true).Cmd(helpers, regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + } - t.Run("3. valid credentials, any url variant, should always succeed", func(t *testing.T) { - t.Parallel() + // 3. valid credentials, any url variant, should always succeed + { c := (&Client{}). WithCredentials(username, password). WithHostsDir(reg.HostsDir). @@ -453,98 +372,108 @@ func TestLoginAgainstVariants(t *testing.T) { WithInsecure(true) // TODO: remove specialization when we fix the localhost mess - rl, _ := dockerconfigresolver.Parse(regHost) - if !rl.IsLocalhost() || !tls { - c.Run(base, "http://"+regHost).AssertOK() - c.Run(base, "https://"+regHost).AssertOK() - c.Run(base, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").AssertOK() - c.Run(base, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").AssertOK() + if !rl.IsLocalhost() || !tc.tls { + c.Cmd(helpers, "http://"+regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "https://"+regHost).Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "http://"+regHost+"/whatever?foo=bar;foo:bar#foo=bar").Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) + c.Cmd(helpers, "https://"+regHost+"/whatever?foo=bar&bar=foo;foo=foo+bar:bar#foo=bar").Run(&test.Expected{ExitCode: expect.ExitCodeSuccess}) } - }) - - t.Run("4. wrong password should always fail", func(t *testing.T) { - t.Parallel() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithHostsDir(reg.HostsDir). - WithInsecure(true). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials(username, "invalid"). - WithInsecure(true). - Run(base, regHost). - AssertFail() - }) - - t.Run("5. wrong username should always fail", func(t *testing.T) { - t.Parallel() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithHostsDir(reg.HostsDir). - WithInsecure(true). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithInsecure(false). - Run(base, regHost). - AssertFail() - - (&Client{}). - WithCredentials("invalid", password). - WithInsecure(true). - Run(base, regHost). - AssertFail() - }) - }) - } + } + + // 4. wrong password should always fail + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithHostsDir(reg.HostsDir). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials(username, "invalid"). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + // 5. wrong username should always fail + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithHostsDir(reg.HostsDir). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithInsecure(false). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + + (&Client{}). + WithCredentials("invalid", password). + WithInsecure(true). + Cmd(helpers, regHost). + Run(&test.Expected{ExitCode: expect.ExitCodeGenericFail}) + } + }, + Cleanup: func(data test.Data, helpers test.Helpers) { + if reg != nil { + reg.Cleanup(data, helpers) + } + if tokenAuthServer != nil { + tokenAuthServer.Cleanup(data, helpers) + } + }, }) } + + testCase := &test.Case{ + Require: require.All( + require.Not(nerdtest.Docker), + nerdtest.Registry, + ), + SubTests: subtests, + } + testCase.Run(t) } diff --git a/cmd/nerdctl/network/network_inspect_test.go b/cmd/nerdctl/network/network_inspect_test.go index 980ade0ad57..84e5e6f9dcb 100644 --- a/cmd/nerdctl/network/network_inspect_test.go +++ b/cmd/nerdctl/network/network_inspect_test.go @@ -62,7 +62,6 @@ func TestNetworkInspectBasic(t *testing.T) { }, { Description: "none", - Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "none"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network @@ -74,7 +73,6 @@ func TestNetworkInspectBasic(t *testing.T) { }, { Description: "host", - Require: nerdtest.NerdctlNeedsFixing("no issue opened"), Command: test.Command("network", "inspect", "host"), Expected: test.Expects(0, nil, func(stdout string, t tig.T) { var dc []dockercompat.Network diff --git a/docs/command-reference.md b/docs/command-reference.md index b32633b2139..2195e6ee7bb 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -1012,6 +1012,10 @@ Flags: - `--zstdchunked-record-in=` : read `ctr-remote optimize --record-out=` record file. :warning: This flag is experimental and subject to change. - `--zstdchunked-compression-level=`: zstd:chunked compression level (default: 3) - `--zstdchunked-chunk-size=`: zstd:chunked chunk size +- `--overlaybd` : convert tar.gz layers to overlaybd layers. Should be used in conjunction with '--oci' +- `--overlaybd-fs-type=` : filesystem type for overlaybd (default: `ext4`) +- `--overlaybd-dbstr=` : database config string for overlaybd +- `--overlaybd-vsize=` : virtual block device size in GB for overlaybd (default: 64) - `--uncompress` : convert tar.gz layers to uncompressed tar layers - `--oci` : convert Docker media types to OCI media types - `--platform=` : convert content for a specific platform diff --git a/docs/overlaybd.md b/docs/overlaybd.md index e6e6284c42b..c6ca36d4bf3 100644 --- a/docs/overlaybd.md +++ b/docs/overlaybd.md @@ -38,3 +38,5 @@ For more details about how to build overlaybd image, please refer to [accelerate Nerdctl supports to convert an OCI image or docker format v2 image to OverlayBD image by using the `nerdctl image convert` command. Before the conversion, you should have the `overlaybd-snapshotter` binary installed, which build from [accelerated-container-image](https://github.com/containerd/accelerated-container-image). You can run the command like `nerdctl image convert --overlaybd --oci ` to convert the `` to a OverlayBD image whose tag is ``. + +By default, `nerdctl image convert --overlaybd` uses a 64 GB virtual block device size. You can customize it with `--overlaybd-vsize`. diff --git a/go.mod b/go.mod index 1d105cbc64f..246436fa89e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/containerd/cgroups/v3 v3.1.3 //gomodjail:unconfined github.com/containerd/console v1.0.5 //gomodjail:unconfined github.com/containerd/containerd/api v1.11.1 - github.com/containerd/containerd/v2 v2.3.1 //gomodjail:unconfined + github.com/containerd/containerd/v2 v2.3.2 //gomodjail:unconfined github.com/containerd/continuity v0.5.0 //gomodjail:unconfined github.com/containerd/errdefs v1.0.0 github.com/containerd/fifo v1.1.0 //gomodjail:unconfined @@ -25,14 +25,14 @@ require ( github.com/containerd/stargz-snapshotter v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/estargz v0.18.2 //gomodjail:unconfined github.com/containerd/stargz-snapshotter/ipfs v0.18.2 //gomodjail:unconfined - github.com/containerd/typeurl/v2 v2.2.3 + github.com/containerd/typeurl/v2 v2.3.0 github.com/containernetworking/cni v1.3.0 //gomodjail:unconfined github.com/containernetworking/plugins v1.9.1 //gomodjail:unconfined github.com/coreos/go-iptables v0.8.0 //gomodjail:unconfined github.com/coreos/go-systemd/v22 v22.7.0 - github.com/cyphar/filepath-securejoin v0.6.1 //gomodjail:unconfined + github.com/cyphar/filepath-securejoin v0.7.0 //gomodjail:unconfined github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.5.2+incompatible //gomodjail:unconfined + github.com/docker/cli v29.6.0+incompatible //gomodjail:unconfined github.com/docker/go-connections v0.7.0 github.com/docker/go-units v0.5.0 github.com/fahedouch/go-logrotate v0.3.0 //gomodjail:unconfined @@ -43,9 +43,9 @@ require ( github.com/ipfs/go-cid v0.6.1 github.com/klauspost/compress v1.18.6 github.com/mattn/go-isatty v0.0.22 //gomodjail:unconfined - github.com/moby/moby/client v0.4.1 - github.com/moby/moby/v2 v2.0.0-beta.15 - github.com/moby/sys/mount v0.3.4 + github.com/moby/moby/client v0.5.0 + github.com/moby/moby/v2 v2.0.0-beta.18 + github.com/moby/sys/mount v0.3.5 github.com/moby/sys/signal v0.7.1 github.com/moby/sys/user v0.4.0 //gomodjail:unconfined github.com/moby/sys/userns v0.1.0 //gomodjail:unconfined @@ -54,22 +54,22 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 github.com/opencontainers/runtime-spec v1.3.0 - github.com/opencontainers/selinux v1.15.0 - github.com/pelletier/go-toml/v2 v2.3.1 + github.com/opencontainers/selinux v1.15.1 + github.com/pelletier/go-toml/v2 v2.4.0 github.com/rootless-containers/bypass4netns v0.4.2 //gomodjail:unconfined - github.com/rootless-containers/rootlesskit/v3 v3.0.0 //gomodjail:unconfined + github.com/rootless-containers/rootlesskit/v3 v3.0.1 //gomodjail:unconfined github.com/spf13/cobra v1.10.2 //gomodjail:unconfined github.com/spf13/pflag v1.0.10 //gomodjail:unconfined github.com/vishvananda/netlink v1.3.1 //gomodjail:unconfined github.com/vishvananda/netns v0.0.5 //gomodjail:unconfined github.com/yuchanns/srslog v1.1.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.52.0 - golang.org/x/net v0.55.0 - golang.org/x/sync v0.20.0 //gomodjail:unconfined - golang.org/x/sys v0.45.0 //gomodjail:unconfined - golang.org/x/term v0.43.0 //gomodjail:unconfined - golang.org/x/text v0.37.0 + golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 + golang.org/x/sync v0.21.0 //gomodjail:unconfined + golang.org/x/sys v0.46.0 //gomodjail:unconfined + golang.org/x/term v0.44.0 //gomodjail:unconfined + golang.org/x/text v0.38.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.0 //gomodjail:unconfined ) @@ -90,7 +90,6 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -104,7 +103,7 @@ require ( github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/sys/mountinfo v0.7.2 // indirect - github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/symlink v0.3.0 // indirect github.com/mr-tron/base58 v1.3.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect @@ -129,16 +128,16 @@ require ( github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect - go.opentelemetry.io/otel v1.43.0 // indirect - go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect + go.opentelemetry.io/otel v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/otel/trace v1.44.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc // indirect - golang.org/x/mod v0.36.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect + golang.org/x/mod v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect //gomodjail:unconfined - google.golang.org/grpc v1.80.0 // indirect + google.golang.org/grpc v1.81.1 // indirect //gomodjail:unconfined google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -148,9 +147,9 @@ require ( ) require ( - cyphar.com/go-pathrs v0.2.1 // indirect + cyphar.com/go-pathrs v0.2.5 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/api v1.55.0 // indirect github.com/moby/sys/capability v0.4.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect sigs.k8s.io/knftables v0.0.18 // indirect diff --git a/go.sum b/go.sum index 19d12545873..de4e8c13c0b 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik= +cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -34,8 +34,8 @@ github.com/containerd/console v1.0.5 h1:R0ymNeydRqH2DmakFNdmjR2k0t7UPuiOV/N/27/q github.com/containerd/console v1.0.5/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU= github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw= -github.com/containerd/containerd/v2 v2.3.1 h1:4dVXBdlvotRBlaP2TmNbY/EGc06KJrMDDUqQdxX/HOk= -github.com/containerd/containerd/v2 v2.3.1/go.mod h1:xVoxGPWZBwwph8DF2IbDhriLKdHfjdpO0b3wFP9wQ1I= +github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM= +github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4= github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg= github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -66,8 +66,8 @@ github.com/containerd/stargz-snapshotter/ipfs v0.18.2 h1:GPkGnRka0GcsiAXh7rN2C+X github.com/containerd/stargz-snapshotter/ipfs v0.18.2/go.mod h1:a7cHDMpUfj5f3aZ3/50jEyapRZGjyaOKSDgFH4eiEnE= github.com/containerd/ttrpc v1.2.8 h1:xbVu6D4qF2jihdh9rDVOKqUMiFBQk6YctTdo1zk087Y= github.com/containerd/ttrpc v1.2.8/go.mod h1:wyZW2K79t4Hfcxl+GUvkZqRBzJlqFFvgEeeWXa42tyE= -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= +github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo= github.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4= github.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA= @@ -81,8 +81,8 @@ github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -93,8 +93,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ= -github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.6.0+incompatible h1:nw9himxMMZ7eIeherJNlKQq+acnlzGgHd+4uf10QRSc= +github.com/docker/cli v29.6.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= @@ -132,8 +132,6 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= @@ -176,8 +174,6 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= @@ -208,20 +204,20 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= -github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= -github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= -github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= -github.com/moby/moby/v2 v2.0.0-beta.15 h1:v9Uk1WYdov0Pdxu68Kp7bhjieH/sis41ARNaNG+6rmk= -github.com/moby/moby/v2 v2.0.0-beta.15/go.mod h1:jXQSSDlWvnbjDsq3HxKOTATchnpeb2G+N8dvR0Fpgj4= +github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc= +github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc= +github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s= +github.com/moby/moby/v2 v2.0.0-beta.18 h1:eOu0ZKNhBbtLmjVVHfEhpM6PEVG0SZZoRhdSLMZo2TY= +github.com/moby/moby/v2 v2.0.0-beta.18/go.mod h1:Br23XQzTa+rD+5bhxB1E6+0CkXTN+jMWKZC8m8rnih8= github.com/moby/sys/capability v0.4.0 h1:4D4mI6KlNtWMCM1Z/K0i7RV1FkX+DBDHKVJpCndZoHk= github.com/moby/sys/capability v0.4.0/go.mod h1:4g9IK291rVkms3LKCDOoYlnV8xKwoDTpIrNEE35Wq0I= -github.com/moby/sys/mount v0.3.4 h1:yn5jq4STPztkkzSKpZkLcmjue+bZJ0u2AuQY1iNI1Ww= -github.com/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mount v0.3.5 h1:eS3fsZTjHaBihwjp4/+5Z3jxqLXYsbwxqpVSfFv3M00= +github.com/moby/sys/mount v0.3.5/go.mod h1:WUQDO+/uCiCIkIztx8SrwIDVn2dtMFRBebRhpDFT71M= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= -github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= -github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/sequential v0.7.0 h1:ASQNGNROJSuOO6LL6bPHbKvuZu6NU8P4ldPWk31zj/8= +github.com/moby/sys/sequential v0.7.0/go.mod h1:NfSTAp6V3fw4tmkD62PEcOKeZKquXT8VKCkf7aVR79o= github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/symlink v0.3.0 h1:GZX89mEZ9u53f97npBy4Rc3vJKj7JBDj/PN2I22GrNU= @@ -260,10 +256,10 @@ github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5 github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116 h1:tAKu3NkKWZYpqBSOJKwTxT1wIGueiF7gcmcNgr5pNTY= github.com/opencontainers/runtime-tools v0.9.1-0.20251114084447-edf4cb3d2116/go.mod h1:DKDEfzxvRkoQ6n9TGhxQgg2IM1lY4aM0eaQP4e3oElw= -github.com/opencontainers/selinux v1.15.0 h1:4Gs40e/R2FvM8PC1HPaPncLLaDor8Y2WDfk5gjU9o5M= -github.com/opencontainers/selinux v1.15.0/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= -github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc= -github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= +github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= +github.com/pelletier/go-toml/v2 v2.4.0 h1:Mwu0mAkUKbittDs3/ADDWXqMmq3EOK2VHiuCkV00Row= +github.com/pelletier/go-toml/v2 v2.4.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= @@ -274,14 +270,14 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rootless-containers/bypass4netns v0.4.2 h1:JUZcpX7VLRfDkLxBPC6fyNalJGv9MjnjECOilZIvKRc= github.com/rootless-containers/bypass4netns v0.4.2/go.mod h1:iOY28IeFVqFHnK0qkBCQ3eKzKQgSW5DtlXFQJyJMAQk= -github.com/rootless-containers/rootlesskit/v3 v3.0.0 h1:esRHLVDYPWcqiPBTDR8gYeJB0kxVturOFYUP7kT2HgA= -github.com/rootless-containers/rootlesskit/v3 v3.0.0/go.mod h1:cAJ5ACtY9npaRpdeT6x1sJgt4gAbYB3/At7qX2LwpII= +github.com/rootless-containers/rootlesskit/v3 v3.0.1 h1:AbYUo1O3b8YFL9X8YuEaaWkVFTwdU551CKyuOt0nJok= +github.com/rootless-containers/rootlesskit/v3 v3.0.1/go.mod h1:7HrjR+SnuEMVvGexEinuuTmhvVW+kxj+6+pkQ/O4ja4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= @@ -327,25 +323,23 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuchanns/srslog v1.1.0 h1:CEm97Xxxd8XpJThE0gc/XsqUGgPufh5u5MUjC27/KOk= github.com/yuchanns/srslog v1.1.0/go.mod h1:HsLjdv3XV02C3kgBW2bTyW6i88OQE+VYJZIxrPKPPak= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= -go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -357,38 +351,33 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U= go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc h1:TS73t7x3KarrNd5qAipmspBDS1rkMcgVG/fS1aRb4Rc= golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= -golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -397,22 +386,20 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -432,8 +419,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -443,8 +430,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -454,16 +441,14 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -471,9 +456,7 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -481,15 +464,15 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= -google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= +google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/hack/provisioning/gpg/hashicorp b/hack/provisioning/gpg/hashicorp deleted file mode 100644 index 495865561d5..00000000000 --- a/hack/provisioning/gpg/hashicorp +++ /dev/null @@ -1,64 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQINBGO9u+MBEADmE9i8rpt8xhRqxbzlBG06z3qe+e1DI+SyjscyVVRcGDrEfo+J -W5UWw0+afey7HFkaKqKqOHVVGSjmh6HO3MskxcpRm/pxRzfni/OcBBuJU2DcGXnG -nuRZ+ltqBncOuONi6Wf00McTWviLKHRrP6oWwWww7sYF/RbZp5xGmMJ2vnsNhtp3 -8LIMOmY2xv9LeKMh++WcxQDpIeRohmSJyknbjJ0MNlhnezTIPajrs1laLh/IVKVz -7/Z73UWX+rWI/5g+6yBSEtj368N7iyq+hUvQ/bL00eyg1Gs8nE1xiCmRHdNjMBLX -lHi0V9fYgg3KVGo6Hi/Is2gUtmip4ZPnThVmB5fD5LzS7Y5joYVjHpwUtMD0V3s1 -HiHAUbTH+OY2JqxZDO9iW8Gl0rCLkfaFDBS2EVLPjo/kq9Sn7vfp2WHffWs1fzeB -HI6iUl2AjCCotK61nyMR33rNuNcbPbp+17NkDEy80YPDRbABdgb+hQe0o8htEB2t -CDA3Ev9t2g9IC3VD/jgncCRnPtKP3vhEhlhMo3fUCnJI7XETgbuGntLRHhmGJpTj -ydudopoMWZAU/H9KxJvwlVXiNoBYFvdoxhV7/N+OBQDLMevB8XtPXNQ8ZOEHl22G -hbL8I1c2SqjEPCa27OIccXwNY+s0A41BseBr44dmu9GoQVhI7TsetpR+qwARAQAB -tFFIYXNoaUNvcnAgU2VjdXJpdHkgKEhhc2hpQ29ycCBQYWNrYWdlIFNpZ25pbmcp -IDxzZWN1cml0eStwYWNrYWdpbmdAaGFzaGljb3JwLmNvbT6JAlQEEwEIAD4CGwMF -CwkIBwIGFQoJCAsCBBYCAwECHgECF4AWIQR5iuxlTlwVQoyOQu6qFvy8piHnAQUC -Y728PQUJCWYB2gAKCRCqFvy8piHnAd16EADeBtTgkdVEvct40TH/9HKkR/Lc/ohM -rer6FFHdKmceJ6Ma8/Qm4nCO5C7c4+EPjsUXdhK5w8DSdC5VbKLJDY1EnDlmU5B1 -wSFkGoYKoB8lUn30E77E33MTu2kfrSuF605vetq269CyBwIJV7oNN6311dW8iQ6z -IytTtlJbVr4YZ7Vst40/uR4myumk9bVBGEd6JhFAPmr/um+BZFhRf9/8xtOryOyB -GF2d+bc9IoAugpxwv0IowHEqkI4RpK2U9hvxG80sTOcmerOuFbmNyPwnEgtJ6CM1 -bc8WAmObJiQcRSLbcgF+a7+2wqrUbCqRE7QoS2wjd1HpUVPmSdJN925c2uaua2A4 -QCbTEg8kV2HiP0HGXypVNhZJt5ouo0YgR6BSbMlsMHniDQaSIP1LgmEz5xD4UAxO -Y/GRR3LWojGzVzBb0T98jpDgPtOu/NpKx3jhSpE2U9h/VRDiL/Pf7gvEIxPUTKuV -5D8VqAiXovlk4wSH13Q05d9dIAjuinSlxb4DVr8IL0lmx9DyHehticmJVooHDyJl -HoA2q2tFnlBBAFbN92662q8Pqi9HbljVRTD1vUjof6ohaoM+5K1C043dmcwZZMTc -7gV1rbCuxh69rILpjwM1stqgI1ONUIkurKVGZHM6N2AatNKqtBRdGEroQo1aL4+4 -u+DKFrMxOqa5b7kCDQRjvbwTARAA0ut7iKLj9sOcp5kRG/5V+T0Ak2k2GSus7w8e -kFh468SVCNUgLJpLzc5hBiXACQX6PEnyhLZa8RAG+ehBfPt03GbxW6cK9nx7HRFQ -GA79H5B4AP3XdEdT1gIL2eaHdQot0mpF2b07GNfADgj99MhpxMCtTdVbBqHY8YEQ -Uq7+E9UCNNs45w5ddq07EDk+o6C3xdJ42fvS2x44uNH6Z6sdApPXLrybeun74C1Z -Oo4Ypre4+xkcw2q2WIhy0Qzeuw+9tn4CYjrhw/+fvvPGUAhtYlFGF6bSebmyua8Q -MTKhwqHqwJxpjftM3ARdgFkhlH1H+PcmpnVutgTNKGcy+9b/lu/Rjq/47JZ+5VkK -ZtYT/zO1oW5zRklHvB6R/OcSlXGdC0mfReIBcNvuNlLhNcBA9frNdOk3hpJgYDzg -f8Ykkc+4z8SZ9gA3g0JmDHY1X3SnSadSPyMas3zH5W+16rq9E+MZztR0RWwmpDtg -Ff1XGMmvc+FVEB8dRLKFWSt/E1eIhsK2CRnaR8uotKW/A/gosao0E3mnIygcyLB4 -fnOM3mnTF3CcRumxJvnTEmSDcoKSOpv0xbFgQkRAnVSn/gHkcbVw/ZnvZbXvvseh -7dstp2ljCs0queKU+Zo22TCzZqXX/AINs/j9Ll67NyIJev445l3+0TWB0kego5Fi -UVuSWkMAEQEAAYkEcgQYAQgAJhYhBHmK7GVOXBVCjI5C7qoW/LymIecBBQJjvbwT -AhsCBQkJZgGAAkAJEKoW/LymIecBwXQgBBkBCAAdFiEE6wr14plJaVlvmYc+cG5m -g2nAhekFAmO9vBMACgkQcG5mg2nAhenPURAAimI0EBZbqpyHpwpbeYq3Pygg1bdo -IlBQUVoutaN1lR7kqGXwYH+BP6G40x79LwVy/fWV8gO7cDX6D1yeKLNbhnJHPBus -FJDmzDPbjTlyWlDqJoWMiPqfAOc1A1cHodsUJDUlA01j1rPTho0S9iALX5R50Wa9 -sIenpfe7RVunDwW5gw6y8me7ncl5trD0LM2HURw6nYnLrxePiTAF1MF90jrAhJDV -+krYqd6IFq5RHKveRtCuTvpL7DlgVCtntmbXLbVC/Fbv6w1xY3A7rXko/03nswAi -AXHKMP14UutVEcLYDBXbDrvgpb2p2ZUJnujs6cNyx9cOPeuxnke8+ACWvpnWxwjL -M5u8OckiqzRRobNxQZ1vLxzdovYTwTlUAG7QjIXVvOk9VNp/ERhh0eviZK+1/ezk -Z8nnPjx+elThQ+r16EM7hD0RDXtOR1VZ0R3OL64AlZYDZz1jEA3lrGhvbjSIfBQk -T6mxKUsCy3YbElcOyuohmPRgT1iVDIZ/1iPL0Q0HGm4+EsWCdH6fAPB7TlHD8z2D -7JCFLihFDWs5lrZyuWMO9nryZiVjJrOLPcStgJYVd/MhRHR4hC6g09bgo25RMJ6f -gyzL4vlEB7aSUih7yjgL9s5DKXP2J71dAhIlF8nnM403R2xEeHyivnyeR/9Ifn7M -PJvUMUuoG+ZANSMkrw//XA31o//TVk9WsLD1Edxt5XZCoR+fS+Vz8ScLwP1d/vQE -OW/EWzeMRG15C0td1lfHvwPKvf2MN+WLenp9TGZ7A1kEHIpjKvY51AIkX2kW5QLu -Y3LBb+HGiZ6j7AaU4uYR3kS1+L79v4kyvhhBOgx/8V+b3+2pQIsVOp79ySGvVwpL -FJ2QUgO15hnlQJrFLRYa0PISKrSWf35KXAy04mjqCYqIGkLsz2qQCY2lGcD5k05z -bBC4TvxwVxv0ftl2C5Bd0ydl/2YM7GfLrmZmTijK067t4OO+2SROT2oYPDsMtZ6S -E8vUXvoGpQ8tf5Nkrn2t0zDG3UDtgZY5UVYnZI+xT7WHsCz//8fY3QMvPXAuc33T -vVdiSfP0aBnZXj6oGs/4Vl1Dmm62XLr13+SMoepMWg2Vt7C8jqKOmhFmSOWyOmRH -UZJR7nKvTpFnL8atSyFDa4o1bk2U3alOscWS8u8xJ/iMcoONEBhItft6olpMVdzP -CTrnCAqMjTSPlQU/9EGtp21KQBed2KdAsJBYuPgwaQeyNIvQEOXmINavl58VD72Y -2T4TFEY8dUiExAYpSodbwBL2fr8DJxOX68WH6e3fF7HwX8LRBjZq0XUwh0KxgHN+ -b9gGXBvgWnJr4NSQGGPiSQVNNHt2ZcBAClYhm+9eC5/VwB+Etg4+1wDmggztiqE= -=FdUF ------END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 29ba08607d9..2053d7dca06 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -139,6 +139,8 @@ type OverlaybdOptions struct { OverlayFsType string // OverlaydbDBStr database config string for overlaybd OverlaydbDBStr string + // OverlaybdVsize virtual block device size in GB for overlaybd + OverlaybdVsize int // #endregion } diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index bf44e02c22c..eb1de8df27d 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -206,6 +206,12 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa if err != nil { return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err } + // Image config labels must be applied before any other label-setting opt: + // containerd.WithImageConfigLabels resets the container labels, so running it + // later would clear labels set by other opts (e.g. the restart policy). + if ensuredImage != nil { + cOpts = append(cOpts, containerd.WithImageConfigLabels(ensuredImage.Image)) + } opts = append(opts, rootfsOpts...) cOpts = append(cOpts, rootfsCOpts...) if options.UserNS != "" { @@ -280,7 +286,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.logConfig.Driver = "json-file" } - restartOpts, err := generateRestartOpts(ctx, client, options.Restart, logConfig.LogURI, options.InRun) + restartOpts, err := generateRestartOpts(ctx, client, options.Restart, logConfig.LogURI) if err != nil { return nil, generateRemoveStateDirFunc(ctx, id, internalLabels), err } @@ -357,7 +363,7 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa internalLabels.healthcheck = healthcheckConfig } - lCOpts, err := withContainerLabels(options.Label, options.LabelFile, ensuredImage) + lCOpts, err := withContainerLabels(options.Label, options.LabelFile) if err != nil { return nil, generateRemoveOrphanedDirsFunc(ctx, id, dataStore, internalLabels), err } @@ -650,15 +656,9 @@ func withNerdctlOCIHook(cmd string, args []string) (oci.SpecOpts, error) { }, nil } -func withContainerLabels(label, labelFile []string, ensuredImage *imgutil.EnsuredImage) ([]containerd.NewContainerOpts, error) { +func withContainerLabels(label, labelFile []string) ([]containerd.NewContainerOpts, error) { var opts []containerd.NewContainerOpts - // add labels defined by image - if ensuredImage != nil { - imageLabelOpts := containerd.WithAdditionalContainerLabels(ensuredImage.ImageConfig.Labels) - opts = append(opts, imageLabelOpts) - } - labelMap, err := readKVStringsMapfFromLabel(label, labelFile) if err != nil { return nil, err @@ -825,11 +825,13 @@ func withInternalLabels(internalLabels internalLabels) (containerd.NewContainerO if len(internalLabels.mountPoints) > 0 { mounts := dockercompatMounts(internalLabels.mountPoints) - mountPointsJSON, err := json.Marshal(mounts) + jsonMountBytes, err := json.Marshal(mounts) if err != nil { + return nil, fmt.Errorf("failed to marshal mounts: %w", err) + } + if err := labels.SetMount(m, jsonMountBytes); err != nil { return nil, err } - m[labels.Mounts] = string(mountPointsJSON) } if internalLabels.macAddress != "" { diff --git a/pkg/cmd/container/run_mount.go b/pkg/cmd/container/run_mount.go index bd0c4a08645..5850cad92f0 100644 --- a/pkg/cmd/container/run_mount.go +++ b/pkg/cmd/container/run_mount.go @@ -335,8 +335,10 @@ func generateMountOpts(ctx context.Context, client *containerd.Client, ensuredIm return nil, nil, nil, err } } - if m, found := ls[labels.Mounts]; found { - err = json.Unmarshal([]byte(m), &vfMountPoints) + + nerdctlMounts := labels.GetMount(ls) + if nerdctlMounts != "" { + err = json.Unmarshal([]byte(nerdctlMounts), &vfMountPoints) if err != nil { return nil, nil, nil, err } diff --git a/pkg/cmd/container/run_restart.go b/pkg/cmd/container/run_restart.go index 5932f2175d5..00416ebdbec 100644 --- a/pkg/cmd/container/run_restart.go +++ b/pkg/cmd/container/run_restart.go @@ -51,7 +51,7 @@ func checkRestartCapabilities(ctx context.Context, client *containerd.Client, re return true, nil } -func generateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string, inRun bool) ([]containerd.NewContainerOpts, error) { +func generateRestartOpts(ctx context.Context, client *containerd.Client, restartFlag, logURI string) ([]containerd.NewContainerOpts, error) { if restartFlag == "" || restartFlag == "no" { return nil, nil } @@ -63,11 +63,7 @@ func generateRestartOpts(ctx context.Context, client *containerd.Client, restart if err != nil { return nil, err } - desireStatus := containerd.Created - if inRun { - desireStatus = containerd.Running - } - opts := []containerd.NewContainerOpts{restart.WithPolicy(policy), restart.WithStatus(desireStatus)} + opts := []containerd.NewContainerOpts{restart.WithPolicy(policy)} if logURI != "" { opts = append(opts, restart.WithLogURIString(logURI)) } diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index 7bb0b926ebb..005309fce92 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -394,6 +394,7 @@ func getOBDConvertOpts(options types.ImageConvertOptions) ([]overlaybdconvert.Op obdOpts := []overlaybdconvert.Option{ overlaybdconvert.WithFsType(options.OverlayFsType), overlaybdconvert.WithDbstr(options.OverlaydbDBStr), + overlaybdconvert.WithVsize(options.OverlaybdVsize), } return obdOpts, nil } diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index a35a83a97f5..6aeff7b565c 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -101,22 +101,43 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio storeOpts = append(storeOpts, transferimage.WithExtraReference(imageRef)) } - w := nopWriteCloser{options.Stdout} + w := &syncWriteCloser{ + Writer: options.Stdout, + done: make(chan struct{}), + } pf, done := transferutil.ProgressHandler(ctx, os.Stderr) defer done() - return client.Transfer(ctx, + err = client.Transfer(ctx, transferimage.NewStore("", storeOpts...), tarchive.NewImageExportStream(w, "", exportOpts...), transfer.WithProgress(pf), ) + if err != nil { + return err + } + + // Wait for the stream copy goroutine to finish writing before returning. + // client.Transfer returns when the server-side export completes, but the + // client-side goroutine in containerd's ImageExportStream.MarshalAny may + // still be copying streamed data to the writer. Without this wait, the + // caller may close the output file while the goroutine is still writing, + // resulting in "file already closed" errors and a truncated tar archive. + <-w.done + + return nil } -type nopWriteCloser struct { +// syncWriteCloser wraps an io.Writer and signals when Close is called, +// allowing the caller to wait until the transfer stream goroutine has +// finished writing all data. +type syncWriteCloser struct { io.Writer + done chan struct{} } -func (nopWriteCloser) Close() error { +func (s *syncWriteCloser) Close() error { + close(s.done) return nil } diff --git a/pkg/cmd/volume/rm.go b/pkg/cmd/volume/rm.go index 4b01564c009..0a41b3f23a5 100644 --- a/pkg/cmd/volume/rm.go +++ b/pkg/cmd/volume/rm.go @@ -92,8 +92,9 @@ func usedVolumes(ctx context.Context, containers []containerd.Container) (map[st } return nil, err } - mountsJSON, ok := l[labels.Mounts] - if !ok { + + mountsJSON := labels.GetMount(l) + if mountsJSON == "" { continue } diff --git a/pkg/composer/run.go b/pkg/composer/run.go index 84807cd6dc6..b6885c85e4d 100644 --- a/pkg/composer/run.go +++ b/pkg/composer/run.go @@ -155,7 +155,7 @@ func (c *Composer) Run(ctx context.Context, ro RunOptions) error { } } if ro.WorkDir != "" { - c.project.WorkingDir = ro.WorkDir + targetSvc.WorkingDir = ro.WorkDir } // `compose run` command does not create any of the ports specified in the service configuration. diff --git a/pkg/containerinspector/containerinspector.go b/pkg/containerinspector/containerinspector.go index a3a77e1e60d..3e6a52d8cfe 100644 --- a/pkg/containerinspector/containerinspector.go +++ b/pkg/containerinspector/containerinspector.go @@ -57,12 +57,17 @@ func Inspect(ctx context.Context, container containerd.Container) (*native.Conta log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect Status") return n, nil } + if st.Status == containerd.Stopped { + n.Process.Pid = 0 + } n.Process.Status = st - netNS, err := InspectNetNS(ctx, n.Process.Pid) - if err != nil { - log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect NetNS") - return n, nil + if st.Status == containerd.Running || st.Status == containerd.Paused { + netNS, err := InspectNetNS(ctx, n.Process.Pid) + if err != nil { + log.G(ctx).WithError(err).WithField("id", id).Warnf("failed to inspect NetNS") + return n, nil + } + n.Process.NetNS = netNS } - n.Process.NetNS = netNS return n, nil } diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 94a8e043069..fa27533f080 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -257,16 +257,6 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i return nil } - _, restartPolicyExist := lab[restart.PolicyLabel] - if restartPolicyExist { - if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil { - return err - } - } - - if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil { - return err - } if oldTask, err := container.Task(ctx, nil); err == nil { if _, err := oldTask.Delete(ctx); err != nil { log.G(ctx).WithError(err).Debug("failed to delete old task") @@ -302,6 +292,17 @@ func Start(ctx context.Context, container containerd.Container, isAttach bool, i return err } + // Set status label running should call after task is started. + _, restartPolicyExist := lab[restart.PolicyLabel] + if restartPolicyExist { + if err := UpdateStatusLabel(ctx, container, containerd.Running); err != nil { + return err + } + } + if err := UpdateExplicitlyStoppedLabel(ctx, container, false); err != nil { + return err + } + // If container has health checks configured, create and start systemd timer/service files. if err := healthcheck.CreateTimer(ctx, container, cfg, nerdctlCmd, nerdctlArgs); err != nil { return fmt.Errorf("failed to create healthcheck timer: %w", err) @@ -583,8 +584,15 @@ func GetContainerVolumes(containerLabels map[string]string) []*ContainerVolume { var vols []*ContainerVolume volLabels := []string{labels.AnonymousVolumes, labels.Mounts} for _, volLabel := range volLabels { - names, ok := containerLabels[volLabel] - if !ok { + var names string + + if volLabel == labels.Mounts { + names = labels.GetMount(containerLabels) + } else { + names = containerLabels[volLabel] + } + + if names == "" { continue } var ( diff --git a/pkg/containerutil/containerutil_test.go b/pkg/containerutil/containerutil_test.go index 88d6c42be94..e45b3bb9773 100644 --- a/pkg/containerutil/containerutil_test.go +++ b/pkg/containerutil/containerutil_test.go @@ -19,6 +19,8 @@ package containerutil import ( "reflect" "testing" + + "github.com/containerd/nerdctl/v2/pkg/labels" ) func TestParseExtraHosts(t *testing.T) { @@ -81,3 +83,40 @@ func TestParseExtraHosts(t *testing.T) { }) } } + +func TestGetContainerVolumes_Indexed(t *testing.T) { + m0 := `{"Type":"volume","Name":"vol-0","Source":"/var/lib/vol-0","Destination":"/mnt/vol-0"}` + m1 := `{"Type":"volume","Name":"vol-1","Source":"/var/lib/vol-1","Destination":"/mnt/vol-1"}` + m2 := `{"Type":"volume","Name":"vol-2","Source":"/var/lib/vol-2","Destination":"/mnt/vol-2"}` + + rawJSON := "[" + m0 + "," + m1 + "," + m2 + "]" + + indexedLabels := map[string]string{ + "nerdctl/mounts.0": m0, + "nerdctl/mounts.1": m1, + "nerdctl/mounts.2": m2, + } + + legacyLabels := map[string]string{ + labels.Mounts: rawJSON, + } + + indexedResult := GetContainerVolumes(indexedLabels) + legacyResult := GetContainerVolumes(legacyLabels) + + if len(indexedResult) == 0 { + t.Fatal("Expected to extract volumes from indexed labels, but got 0 results") + } + + if len(indexedResult) != len(legacyResult) { + t.Errorf("Mismatched output! Indexed found %d volumes, Legacy found %d volumes.", + len(indexedResult), len(legacyResult)) + } + + if indexedResult[0].Name != "vol-0" { + t.Errorf("Expected first volume to be named 'vol-0', got '%s'", indexedResult[0].Name) + } + if len(indexedResult) > 2 && indexedResult[2].Name != "vol-2" { + t.Errorf("Expected third volume to be named 'vol-2', got '%s'", indexedResult[2].Name) + } +} diff --git a/pkg/formatter/formatter.go b/pkg/formatter/formatter.go index ff25312f9a2..c0201450c30 100644 --- a/pkg/formatter/formatter.go +++ b/pkg/formatter/formatter.go @@ -97,18 +97,19 @@ func Ellipsis(str string, maxDisplayWidth int) string { return "" } - lenStr := len(str) + runes := []rune(str) + lenStr := len(runes) if maxDisplayWidth == 1 { if lenStr <= maxDisplayWidth { return str } - return string(str[0]) + return string(runes[0]) } if lenStr <= maxDisplayWidth { return str } - return str[:maxDisplayWidth-1] + "…" + return string(runes[:maxDisplayWidth-1]) + "…" } func formatRange(startHost, endHost, startContainer, endContainer int32) string { diff --git a/pkg/formatter/formatter_test.go b/pkg/formatter/formatter_test.go index 9da5e6fcf11..dda59e80fe0 100644 --- a/pkg/formatter/formatter_test.go +++ b/pkg/formatter/formatter_test.go @@ -192,3 +192,48 @@ func TestFormatPorts(t *testing.T) { }) } } + +func TestEllipsis(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + maxDisplayWidth int + expected string + }{ + { + name: "ascii under limit", + input: "hello", + maxDisplayWidth: 5, + expected: "hello", + }, + { + name: "ascii truncated", + input: "hello", + maxDisplayWidth: 4, + expected: "hel…", + }, + { + name: "unicode truncated", + input: "éclair", + maxDisplayWidth: 4, + expected: "écl…", + }, + { + name: "unicode truncated to single rune", + input: "éclair", + maxDisplayWidth: 1, + expected: "é", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + result := Ellipsis(tt.input, tt.maxDisplayWidth) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/healthcheck/executor.go b/pkg/healthcheck/executor.go index 2525ee7c54b..e86c65e7f4c 100644 --- a/pkg/healthcheck/executor.go +++ b/pkg/healthcheck/executor.go @@ -75,6 +75,11 @@ func probeHealthCheck(ctx context.Context, task containerd.Task, hc *Healthcheck log.G(ctx).Debugf("failed to exec health check: %v", err) return nil, fmt.Errorf("exec error: %w", err) } + defer func() { + if _, err := process.Delete(ctx); err != nil { + log.G(ctx).WithError(err).Debug("failed to delete exec process") + } + }() if err := process.Start(ctx); err != nil { log.G(ctx).Debugf("failed to start health check: %v", err) diff --git a/pkg/infoutil/infoutil.go b/pkg/infoutil/infoutil.go index 2dee6f88a85..16d0b1f1379 100644 --- a/pkg/infoutil/infoutil.go +++ b/pkg/infoutil/infoutil.go @@ -35,6 +35,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/buildkitutil" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" + "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/version" ) @@ -142,6 +143,9 @@ func ServerVersion(ctx context.Context, client *containerd.Client) (*dockercompa runcVersion(), }, } + if rootlessutil.IsRootless() { + v.Components = append(v.Components, rootlessKitVersion(ctx)) + } return v, nil } @@ -233,6 +237,35 @@ func parseRuncVersion(runcVersionStdout []byte) (*dockercompat.ComponentVersion, } // getMobySysInfo returns the moby system info for the given cgroup manager + +func rootlessKitVersion(ctx context.Context) dockercompat.ComponentVersion { + rc, err := rootlessutil.NewRootlessKitClient() + if err != nil { + log.L.WithError(err).Warnf("unable to connect to RootlessKit API socket") + return dockercompat.ComponentVersion{Name: "rootlesskit"} + } + info, err := rc.Info(ctx) + if err != nil { + log.L.WithError(err).Warnf("unable to retrieve RootlessKit version via API") + return dockercompat.ComponentVersion{Name: "rootlesskit"} + } + details := map[string]string{ + "ApiVersion": info.APIVersion, + "StateDir": info.StateDir, + } + if info.NetworkDriver != nil { + details["NetworkDriver"] = info.NetworkDriver.Driver + } + if info.PortDriver != nil { + details["PortDriver"] = info.PortDriver.Driver + } + return dockercompat.ComponentVersion{ + Name: "rootlesskit", + Version: info.Version, + Details: details, + } +} + func getMobySysInfo(cgroupManager string) *sysinfo.SysInfo { var info dockercompat.Info info.CgroupVersion = CgroupsVersion() diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index 7bb1f4ea9b7..7626c3f5b92 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -400,7 +400,7 @@ func ContainerFromNative(n *native.Container) (*Container, error) { } c.HostConfig.Tmpfs = make(map[string]string) - if nerdctlMounts := n.Labels[labels.Mounts]; nerdctlMounts != "" { + if nerdctlMounts := labels.GetMount(n.Labels); nerdctlMounts != "" { mounts, err := parseMounts(nerdctlMounts) if err != nil { return nil, err diff --git a/pkg/inspecttypes/dockercompat/dockercompat_test.go b/pkg/inspecttypes/dockercompat/dockercompat_test.go index 945228d7323..4c9986ca3b5 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat_test.go +++ b/pkg/inspecttypes/dockercompat/dockercompat_test.go @@ -71,7 +71,7 @@ func TestContainerFromNative(t *testing.T) { n: &native.Container{ Container: containers.Container{ Labels: map[string]string{ - "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/mounts.0": "{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}", "nerdctl/state-dir": tempStateDir, "nerdctl/hostname": "host1", "nerdctl/user": "test-user", @@ -164,7 +164,7 @@ func TestContainerFromNative(t *testing.T) { }, Config: &Config{ Labels: map[string]string{ - "nerdctl/mounts": "[{\"Type\":\"bind\",\"Source\":\"/mnt/foo\",\"Destination\":\"/mnt/foo\",\"Mode\":\"rshared,rw\",\"RW\":true,\"Propagation\":\"rshared\"}]", + "nerdctl/mounts.0": `{"Type":"bind","Source":"/mnt/foo","Destination":"/mnt/foo","Mode":"rshared,rw","RW":true,"Propagation":"rshared"}`, "nerdctl/state-dir": tempStateDir, "nerdctl/hostname": "host1", "nerdctl/user": "test-user", diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 46ca0e9c4a4..eaec0720efb 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -86,6 +86,9 @@ const ( // Mounts is the mount points for the container. Mounts = Prefix + "mounts" + // MountsKeyFormat is used to dynamically store individual mounts + MountsKeyFormat = Prefix + "mounts.%d" + // StopTimeout is seconds to wait for stop a container. StopTimeout = Prefix + "stop-timeout" diff --git a/pkg/labels/mount.go b/pkg/labels/mount.go new file mode 100644 index 00000000000..6a95d6069eb --- /dev/null +++ b/pkg/labels/mount.go @@ -0,0 +1,70 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package labels + +import ( + "encoding/json" + "fmt" + "strings" +) + +// SetMount parses a JSON array of mounts and stores each individual mount object as an indexed label. +func SetMount(m map[string]string, jsonMountBytes []byte) error { + if len(jsonMountBytes) == 0 || string(jsonMountBytes) == "null" || string(jsonMountBytes) == "[]" { + return nil + } + + var rawMounts []json.RawMessage + if err := json.Unmarshal(jsonMountBytes, &rawMounts); err != nil { + // Fallback: If it's somehow not a slice, write the whole thing to the legacy key + m[Mounts] = string(jsonMountBytes) + return nil + } + + for i, rawMount := range rawMounts { + key := fmt.Sprintf(MountsKeyFormat, i) + m[key] = string(rawMount) + } + + return nil +} + +// GetMount extracts and reassembles indexed mount metadata back into a single JSON array string +func GetMount(containerLabels map[string]string) string { + // Try legacy label first for backward compatibility + if legacyMount, ok := containerLabels[Mounts]; ok && legacyMount != "" { + return legacyMount + } + + var rawMounts []string + + for i := 0; i < len(containerLabels); i++ { + key := fmt.Sprintf(MountsKeyFormat, i) + chunk, found := containerLabels[key] + + if !found || chunk == "" { + break + } + rawMounts = append(rawMounts, chunk) + } + + if len(rawMounts) > 0 { + return "[" + strings.Join(rawMounts, ",") + "]" + } + + return "" +} diff --git a/pkg/mountutil/mountutil_linux_test.go b/pkg/mountutil/mountutil_linux_test.go index 80e21542cea..74484cbbbfe 100644 --- a/pkg/mountutil/mountutil_linux_test.go +++ b/pkg/mountutil/mountutil_linux_test.go @@ -259,6 +259,10 @@ func TestProcessFlagV(t *testing.T) { rawSpec: `/mnt/foo:./foo`, err: "expected an absolute path, got \"./foo\"", }, + { + rawSpec: `./:/`, + err: "invalid specification: destination can't be '/'", + }, } for _, tt := range tests { diff --git a/pkg/mountutil/mountutil_unix.go b/pkg/mountutil/mountutil_unix.go index 5bf7e4d2420..32ed5548ec7 100644 --- a/pkg/mountutil/mountutil_unix.go +++ b/pkg/mountutil/mountutil_unix.go @@ -16,9 +16,17 @@ limitations under the License. */ +/* + Portions from https://github.com/moby/moby/blob/docker-v29.5.2/daemon/volume/mounts/linux_parser.go + Copyright (C) Docker/Moby authors. + Licensed under the Apache License, Version 2.0 + NOTICE: https://github.com/moby/moby/blob/docker-v29.5.2/NOTICE +*/ + package mountutil import ( + "errors" "fmt" "path/filepath" "strings" @@ -26,6 +34,8 @@ import ( "github.com/containerd/nerdctl/v2/pkg/mountutil/volumestore" ) +var ErrVolumeTargetIsRoot = errors.New("invalid specification: destination can't be '/'") + func splitVolumeSpec(s string) ([]string, error) { s = strings.TrimLeft(s, ":") split := strings.Split(s, ":") @@ -48,7 +58,17 @@ func cleanMount(p string) string { return filepath.Clean(p) } +func validateNotRoot(p string) error { + if cleanMount(p) == "/" { + return ErrVolumeTargetIsRoot + } + return nil +} + func isValidPath(s string) (bool, error) { + if err := validateNotRoot(s); err != nil { + return false, err + } if filepath.IsAbs(s) { return true, nil } diff --git a/pkg/mountutil/mountutil_windows.go b/pkg/mountutil/mountutil_windows.go index 98fe6471a63..d036d420580 100644 --- a/pkg/mountutil/mountutil_windows.go +++ b/pkg/mountutil/mountutil_windows.go @@ -161,7 +161,18 @@ func cleanMount(p string) string { return filepath.Clean(p) } +func validateNotRoot(p string) error { + p = strings.ToLower(cleanMount(p)) + if p == "c:" || p == `c:\` { + return fmt.Errorf(`destination path (%v) cannot be 'c:' or 'c:\'`, p) + } + return nil +} + func isValidPath(s string) (bool, error) { + if err := validateNotRoot(s); err != nil { + return false, err + } if isNamedPipe(s) || filepath.IsAbs(s) { return true, nil } diff --git a/pkg/mountutil/mountutil_windows_test.go b/pkg/mountutil/mountutil_windows_test.go index 05428b113c5..aeb55d2bb2c 100644 --- a/pkg/mountutil/mountutil_windows_test.go +++ b/pkg/mountutil/mountutil_windows_test.go @@ -262,6 +262,10 @@ func TestProcessFlagV(t *testing.T) { rawSpec: `C:\TestVolume\Path:TestVolume`, err: "expected an absolute path or a named pipe, got \"TestVolume\"", }, + { + rawSpec: `C:\TestVolume\Path:c:\.`, + err: "destination path (c:\\) cannot be 'c:' or 'c:\\'", + }, } for _, tt := range tests { diff --git a/pkg/netutil/cni_plugin.go b/pkg/netutil/cni_plugin.go index f4d65359f2e..b44e76042e2 100644 --- a/pkg/netutil/cni_plugin.go +++ b/pkg/netutil/cni_plugin.go @@ -20,6 +20,14 @@ type CNIPlugin interface { GetPluginType() string } +type pseudoNetworkPlugin struct { + PluginType string `json:"type"` +} + +func (p *pseudoNetworkPlugin) GetPluginType() string { + return p.PluginType +} + type IPAMRange struct { Subnet string `json:"subnet"` RangeStart string `json:"rangeStart,omitempty"` diff --git a/pkg/netutil/netutil.go b/pkg/netutil/netutil.go index c3312bb5191..5f213d8692f 100644 --- a/pkg/netutil/netutil.go +++ b/pkg/netutil/netutil.go @@ -60,8 +60,17 @@ func (e *CNIEnv) ListNetworksMatch(reqs []string, allowPseudoNetwork bool) (list list = make(map[string][]*NetworkConfig) for _, req := range reqs { - if !allowPseudoNetwork && (req == "host" || req == "none") { - errs = append(errs, fmt.Errorf("pseudo network not allowed: %s", req)) + if req == "host" || req == "none" { + if !allowPseudoNetwork { + errs = append(errs, fmt.Errorf("pseudo network not allowed: %s", req)) + continue + } + cfg, err := newPseudoNetworkConfig(req) + if err != nil { + errs = append(errs, err) + continue + } + list[req] = []*NetworkConfig{cfg} continue } @@ -88,6 +97,30 @@ func (e *CNIEnv) ListNetworksMatch(reqs []string, allowPseudoNetwork bool) (list return list, errs } +func newPseudoNetworkConfig(name string) (*NetworkConfig, error) { + confJSON, err := json.Marshal(&cniNetworkConfig{ + CNIVersion: "1.0.0", + Name: name, + // Pseudo networks are not backed by real CNI config files. We still need a + // parseable config object so network inspect can render them consistently. + Plugins: []CNIPlugin{ + &pseudoNetworkPlugin{PluginType: "nerdctl-pseudo"}, + }, + }) + if err != nil { + return nil, err + } + + confList, err := libcni.ConfListFromBytes(confJSON) + if err != nil { + return nil, err + } + + return &NetworkConfig{ + NetworkConfigList: confList, + }, nil +} + func UsedNetworks(ctx context.Context, client *containerd.Client) (map[string][]string, error) { nsService := client.NamespaceService() nsList, err := nsService.List(ctx) @@ -288,8 +321,8 @@ type NetworkConfig struct { type cniNetworkConfig struct { CNIVersion string `json:"cniVersion"` Name string `json:"name"` - ID string `json:"nerdctlID"` - Labels map[string]string `json:"nerdctlLabels"` + ID string `json:"nerdctlID,omitempty"` + Labels map[string]string `json:"nerdctlLabels,omitempty"` Plugins []CNIPlugin `json:"plugins"` } diff --git a/pkg/netutil/netutil_test.go b/pkg/netutil/netutil_test.go index 383054ebd3e..f818c59a1b2 100644 --- a/pkg/netutil/netutil_test.go +++ b/pkg/netutil/netutil_test.go @@ -379,3 +379,33 @@ func TestFSExistsPropagatesStatError(t *testing.T) { assert.Assert(t, !exists) assert.Assert(t, err != nil) } + +func TestListNetworksMatchIncludesPseudoNetworks(t *testing.T) { + cniConfTestDir := t.TempDir() + cniEnv := CNIEnv{ + Path: t.TempDir(), + NetconfPath: cniConfTestDir, + } + + values := map[string]string{ + "network_name": "regular-network", + "subnet": "10.7.1.0/24", + "gateway": "10.7.1.1", + } + tpl, err := template.New("test").Parse(preExistingNetworkConfigTemplate) + assert.NilError(t, err) + buf := &bytes.Buffer{} + assert.NilError(t, tpl.ExecuteTemplate(buf, "test", values)) + + testConfFile := filepath.Join(cniConfTestDir, fmt.Sprintf("%s.conf", testutil.Identifier(t))) + assert.NilError(t, filesystem.WriteFile(testConfFile, buf.Bytes(), 0600)) + + matches, errs := cniEnv.ListNetworksMatch([]string{"host", "none", "regular-network"}, true) + assert.Assert(t, len(errs) == 0) + assert.Equal(t, len(matches["host"]), 1) + assert.Equal(t, matches["host"][0].Name, "host") + assert.Equal(t, len(matches["none"]), 1) + assert.Equal(t, matches["none"][0].Name, "none") + assert.Equal(t, len(matches["regular-network"]), 1) + assert.Equal(t, matches["regular-network"][0].Name, "regular-network") +} diff --git a/pkg/testutil/nerdtest/requirements.go b/pkg/testutil/nerdtest/requirements.go index 7fc0ff005de..2be8b6f9b4c 100644 --- a/pkg/testutil/nerdtest/requirements.go +++ b/pkg/testutil/nerdtest/requirements.go @@ -37,6 +37,7 @@ import ( "github.com/containerd/nerdctl/v2/pkg/containerdutil" ncdefaults "github.com/containerd/nerdctl/v2/pkg/defaults" "github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat" + "github.com/containerd/nerdctl/v2/pkg/inspecttypes/native" "github.com/containerd/nerdctl/v2/pkg/netutil" "github.com/containerd/nerdctl/v2/pkg/rootlessutil" "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" @@ -196,6 +197,44 @@ var RootlessWithoutDetachNetNS = require.All(Rootless, require.Not(RootlessWithD // Rootful marks a test as suitable only for rootful env var Rootful = require.Not(Rootless) +// ContainerdPlugin requires that the given containerd plugin (and capabilities) is available. +var ContainerdPlugin = func(requiredType, requiredID string, requiredCaps []string) *test.Requirement { + return &test.Requirement{ + Check: func(data test.Data, helpers test.Helpers) (bool, string) { + if IsDocker() { + return false, "ContainerdPlugin is not applicable for Docker" + } + stdout := helpers.Capture("info", "--mode", "native", "--format", "{{ json . }}") + var info native.Info + if err := json.Unmarshal([]byte(stdout), &info); err != nil { + return false, fmt.Sprintf("failed to parse info: %v", err) + } + if info.Daemon == nil || info.Daemon.Plugins == nil { + return false, fmt.Sprintf("test requires containerd plugin %q.%q", requiredType, requiredID) + } + for _, p := range info.Daemon.Plugins.Plugins { + if p.Type != requiredType || p.ID != requiredID { + continue + } + capMap := make(map[string]struct{}, len(p.Capabilities)) + for _, c := range p.Capabilities { + capMap[c] = struct{}{} + } + for _, c := range requiredCaps { + if _, ok := capMap[c]; !ok { + return false, fmt.Sprintf("test requires containerd plugin %q.%q with capability %q", requiredType, requiredID, c) + } + } + return true, "" + } + if len(requiredCaps) == 0 { + return false, fmt.Sprintf("test requires containerd plugin %q.%q", requiredType, requiredID) + } + return false, fmt.Sprintf("test requires containerd plugin %q.%q with capabilities %v", requiredType, requiredID, requiredCaps) + }, + } +} + // Info requires that `nerdctl info` satisfies the condition function passed as argument. func Info(f func(dockercompat.Info) error) *test.Requirement { return &test.Requirement{ diff --git a/pkg/testutil/testsyslog/testsyslog.go b/pkg/testutil/testsyslog/testsyslog.go index a5eeb0cbf37..401b3192b0f 100644 --- a/pkg/testutil/testsyslog/testsyslog.go +++ b/pkg/testutil/testsyslog/testsyslog.go @@ -109,6 +109,8 @@ func runPacketSyslog(c net.PacketConn, done chan<- string) { var buf [4096]byte var rcvd string ct := 0 + // 20 retries (2s) to wait for the first packet; drop to 3 (400ms drain) after. + maxRetries := 20 for { var n int var err error @@ -116,9 +118,13 @@ func runPacketSyslog(c net.PacketConn, done chan<- string) { _ = c.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) n, _, err = c.ReadFrom(buf[:]) rcvd += string(buf[:n]) + if n > 0 { + maxRetries = 3 + ct = 0 + } if err != nil { if oe, ok := err.(*net.OpError); ok { - if ct < 3 && oe.Temporary() { + if ct < maxRetries && oe.Temporary() { ct++ continue }